Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/agent/agent-system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,49 @@ export function getInteractiveSystemPrompt(
If you cannot verify a claim, do not make it.
</verification>

<!-- EXISTING REVIEW COMMENTS POLICY -->
<existingCommentsPolicy>
When other reviewers have already commented on the PR (shown in <existingReviewComments>):

1. **DO NOT DUPLICATE**: Never repeat feedback that another reviewer has already given.
If someone already pointed out an issue, do not post the same comment.

2. **USE EMOJI REACTIONS**: When you agree with an existing comment but have nothing
substantial to add, use an emoji reaction instead of posting a new comment.
This is a lightweight way to show your agreement without adding noise.

The comment ID is provided in each <comment> element's \`<commentId>\` field.
Use it to add a reaction:
\`gh api repos/OWNER/REPO/pulls/comments/COMMENT_ID/reactions -f content="+1"\`

Available reactions:
• \`+1\` (👍) - Agree with the comment
• \`-1\` (👎) - Disagree (prefer posting a counter-opinion with explanation)
• \`heart\` (❤️) - Great catch/suggestion
• \`rocket\` (🚀) - Excellent improvement
• \`eyes\` (👀) - Interesting point, needs attention
• \`confused\` (😕) - Unclear or questionable suggestion

3. **REINFORCE when valuable**: If you strongly agree with an existing comment and have
additional context or a stronger argument, you MAY add a supporting comment.
Example: "I agree with @reviewer's point about X. Additionally, this could cause Y..."

4. **RESPECTFULLY DISAGREE**: If you believe an existing comment is incorrect or
the suggested change would be harmful, you may respectfully provide a counter-opinion.
Be constructive and explain your reasoning with evidence.
Example: "I have a different perspective on @reviewer's suggestion about X.
The current approach is actually preferred because..."

5. **FACTOR INTO ASSESSMENT**: Consider existing comments when deciding your overall
review verdict. If issues have already been raised that warrant changes, acknowledge
them in your review summary even if you don't add new comments about them.

6. **STILL DO A FULL REVIEW**: You should review ALL the code in the diff and form your
own judgement about everything. Existing comments don't mean you skip those areas.
Just avoid posting duplicate feedback for issues already raised - use emoji reactions
or add to the discussion with new insights instead.
</existingCommentsPolicy>

<!-- PROACTIVE ANALYSIS - USE YOUR TOOLS -->
<proactiveAnalysis>
Don't just read the diff - actively investigate using your tools:
Expand Down
47 changes: 45 additions & 2 deletions src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Agent, Runner } from "@openai/agents";
import { aisdk } from "@openai/agents-extensions";
import { debugError, debugLog } from "../debug";
import { createModel } from "../model-factory";
import { ModelConfig } from "../types";
import { ExistingReviewComment, ModelConfig } from "../types";
import { getInteractiveSystemPrompt } from "./agent-system-prompt";
import { allTools, resetTodoList } from "./tools";

Expand Down Expand Up @@ -38,6 +38,41 @@ export interface PRContext {
commitSha: string;
}

/**
* Formats existing review comments into a readable string for the agent.
*/
function formatExistingComments(comments: ExistingReviewComment[]): string {
if (comments.length === 0) {
return "";
}

const formattedComments = comments.map((comment, index) => {
const lineInfo = comment.line ? `Line ${comment.line}` : "File-level";
return `<comment index="${index + 1}" id="${comment.id}">
<author>${comment.author}</author>
<file>${comment.path}</file>
<location>${lineInfo}</location>
<commentId>${comment.id}</commentId>
<body>${comment.body}</body>
<codeContext>
${comment.diffHunk}
</codeContext>
</comment>`;
});

return `
<existingReviewComments count="${comments.length}">
These are review comments made by other reviewers on this PR.
Consider them in your review:
- DO NOT duplicate feedback that has already been given
- If you agree with a comment, you may reinforce it or add additional context
- If you disagree with a comment, you may respectfully provide a counter-opinion
- Factor these comments into your overall assessment of the PR

${formattedComments.join("\n\n")}
</existingReviewComments>`;
}

/**
* Reviews an entire PR diff using a single interactive agent.
* The agent has full autonomy to:
Expand All @@ -53,6 +88,7 @@ export async function reviewFullDiff(
prContext: PRContext,
maxTurns: number = 75,
blockingOnly: boolean = false,
existingComments: ExistingReviewComment[] = [],
): Promise<void> {
// Reset todo list for fresh review
resetTodoList();
Expand All @@ -76,6 +112,12 @@ export async function reviewFullDiff(
);
}

// Format existing comments for inclusion
const existingCommentsSection = formatExistingComments(existingComments);
if (existingComments.length > 0) {
debugLog(`📝 Including ${existingComments.length} existing review comments in context`);
}

const initialMessage = `
You are reviewing PR #${prContext.prNumber} in repository ${prContext.repo}.
Commit SHA: ${prContext.commitSha}
Expand All @@ -87,6 +129,7 @@ ${fileList}
<fullDiff>
${fullDiff}
</fullDiff>
${existingCommentsSection}

<instruction>
Please review this pull request. You have the complete diff above.
Expand All @@ -98,7 +141,7 @@ Please review this pull request. You have the complete diff above.
- Run \`gh pr view ${prContext.prNumber} --json body -q '.body'\` to check if description is blank
- **If body is empty/blank, you MUST update it immediately:**
\`gh pr edit ${prContext.prNumber} --body "## Summary\\n\\n<describe what this PR does based on the diff>\\n\\n## Changes\\n\\n- <list key changes>"\`
- Run \`gh api repos/${prContext.repo}/pulls/${prContext.prNumber}/comments\` to check existing comments (avoid duplicates)
- Review the <existingReviewComments> section above (if present) to understand what other reviewers have already commented on

2. **Deep review each changed file:**
For each file in the diff:
Expand Down
43 changes: 43 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as core from "@actions/core";
import * as github from "@actions/github";
import { writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { ExistingReviewComment } from "./types";

interface TriggerConfig {
runOnPrOpened: boolean;
Expand Down Expand Up @@ -327,6 +328,48 @@ async function run(): Promise<void> {
core.info(
`Generated diff file: ${diffFile} (${diffOutput.length} bytes)`,
);

// Fetch existing review comments from other reviewers
const existingCommentsFile = resolve("pr-comments.json");
try {
const { data: reviewComments } =
await octokit.rest.pulls.listReviewComments({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});

// Filter out bot comments and map to our interface
const existingComments: ExistingReviewComment[] = reviewComments
.filter(
(comment) =>
comment.user &&
!comment.user.login.includes("[bot]") &&
comment.user.login !== "github-actions",
)
.map((comment) => ({
id: comment.id,
author: comment.user?.login || "unknown",
body: comment.body,
path: comment.path,
line: comment.line ?? comment.original_line ?? null,
diffHunk: comment.diff_hunk,
createdAt: comment.created_at,
}));

writeFileSync(
existingCommentsFile,
JSON.stringify(existingComments, null, 2),
);
core.info(
`Found ${existingComments.length} existing review comments from other reviewers`,
);
} catch (commentError) {
core.warning(
`Failed to fetch existing comments (continuing without them): ${commentError}`,
);
writeFileSync(existingCommentsFile, "[]");
}
} catch (error) {
core.setFailed(`Failed to fetch diff from GitHub API: ${error}`);
return;
Expand Down
17 changes: 16 additions & 1 deletion src/review-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { resolve } from "path";
import { PRContext, reviewFullDiff } from "./agent";
import { getModelConfig } from "./config";
import { debugLog } from "./debug";
import type { ReviewConfig } from "./types";
import type { ExistingReviewComment, ReviewConfig } from "./types";

/**
* Service class that orchestrates the review process.
Expand Down Expand Up @@ -81,6 +81,20 @@ export class ReviewService {
console.error("COMMIT_SHA not set - agent will not be able to post inline comments");
}

// Load existing review comments from other reviewers
const commentsFile = resolve("pr-comments.json");
let existingComments: ExistingReviewComment[] = [];
if (existsSync(commentsFile)) {
try {
existingComments = JSON.parse(readFileSync(commentsFile, "utf8"));
if (existingComments.length > 0) {
debugLog(`📝 Found ${existingComments.length} existing review comments from other reviewers`);
}
} catch (error) {
debugLog("⚠️ Failed to parse existing comments file, continuing without them");
}
}

// Run the autonomous agent review
debugLog("🚀 Starting autonomous PR review...");
const modelConfig = getModelConfig();
Expand All @@ -93,6 +107,7 @@ export class ReviewService {
prContext,
this.config.maxTurns,
this.config.blockingOnly,
existingComments,
);
debugLog("✅ Review completed!");
} catch (error: unknown) {
Expand Down
21 changes: 21 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,24 @@ export interface ParsedArgs {
diff: string;
pr: number;
}

/**
* Represents an existing review comment on the PR.
* These are comments made by other reviewers that the bot should be aware of.
*/
export interface ExistingReviewComment {
/** GitHub comment ID (for adding reactions) */
id: number;
/** GitHub username of the commenter */
author: string;
/** The comment body/text */
body: string;
/** File path the comment is on */
path: string;
/** Line number in the new version of the file (may be null for file-level comments) */
line: number | null;
/** The diff hunk context around the comment */
diffHunk: string;
/** ISO timestamp when the comment was created */
createdAt: string;
}
2 changes: 2 additions & 0 deletions test/review-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ diff --git a/src/test.ts b/src/test.ts
}), // prContext
20, // maxTurns
false, // blockingOnly
[], // existingComments (empty when no pr-comments.json)
);
});

Expand All @@ -107,6 +108,7 @@ diff --git a/src/test.ts b/src/test.ts
expect.any(Object),
20,
true, // blockingOnly should be true
[], // existingComments
);
});

Expand Down