-
Notifications
You must be signed in to change notification settings - Fork 0
feat: AI-driven PR comment resolution #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
cbb2114
feat: implement PR comment resolution workflow with AI analysis
valuecodes 00573bb
docs: update README.md with PR comment resolver CLI details
valuecodes 06a64a1
feat: enhance PR comment resolution with status tracking
valuecodes febd760
fix: update PR comment resolution logic for empty comments
valuecodes 2af9743
feat: enhance PR comment resolution with thread indexing and truncation
valuecodes 493c717
fix: remove API key check from PR comment resolution condition
valuecodes cb9a9a7
refactor: simplify PR comment resolution by removing base branch argu…
valuecodes 39bfac0
feat: enhance PR comment resolution with reaction tracking
valuecodes 32cc24c
refactor: simplify reaction checks in comment methods
valuecodes 9a6b827
refactor: format code
valuecodes 6b2abe8
docs: update reply tone guidelines for PR comments
valuecodes 2e13cf8
refactor: filter out bot comments in PR comment resolution
valuecodes 3412f35
feat: add support for previously uncertain comment tracking with 👀 re…
valuecodes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| # Resolve PR Comments CLI | ||
|
|
||
| Analyze review comments on a GitHub PR against the current diff, then reply to addressed or uncertain comments and resolve those marked addressed. | ||
|
|
||
| ## Run | ||
|
|
||
| ```bash | ||
| pnpm run:resolve-pr-comments -- --pr=10 | ||
| pnpm run:resolve-pr-comments -- --pr=10 --repo=owner/repo | ||
| pnpm run:resolve-pr-comments -- --pr=10 --base=main | ||
| pnpm run:resolve-pr-comments -- --pr=10 --dry-run | ||
| ``` | ||
|
|
||
| ## Arguments | ||
|
|
||
| - `--pr` (required): PR number to analyze. | ||
| - `--repo` (optional): Repository in `owner/repo` format. Defaults to `gh repo view`. | ||
| - `--base` (default: `main`): Base ref for `git diff <base>...HEAD`. | ||
| - `--dry-run` (default: false): Log replies/resolutions without posting. | ||
|
|
||
| ## Workflow | ||
|
|
||
| 1. Ensures `gh` is installed and authenticated | ||
| 2. Resolves repo (`--repo` override or `gh repo view`) | ||
| 3. Fetches inline review comments for the PR | ||
| 4. Gets the git diff for `base...HEAD` | ||
| 5. Analyzes comments vs diff with a single AI request | ||
| 6. Writes analysis JSON under `tmp/resolve-pr-comments/pr-<number>/` | ||
| 7. Replies to addressed or uncertain comments and resolves addressed ones (skipped in dry-run) | ||
|
|
||
| ## Output | ||
|
|
||
| Writes analysis to: | ||
|
|
||
| ``` | ||
| tmp/resolve-pr-comments/pr-<number>/analysis.json | ||
| ``` | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - `gh` CLI installed and authenticated (`gh auth login`) | ||
| - `OPENAI_API_KEY` set for the analysis model | ||
| - Local git checkout with the base ref available (e.g., `main`) | ||
|
|
||
| ## Flowchart | ||
|
|
||
| ```mermaid | ||
| flowchart TD | ||
| A["Start"] --> B["Parse args"] | ||
| B --> C["Check gh auth + resolve repo"] | ||
| C --> D["Fetch review comments"] | ||
| D --> E["Get git diff base...HEAD"] | ||
| E --> F["AI analyze comments vs diff"] | ||
| F --> G["Write analysis.json"] | ||
| G --> H["Reply + resolve addressed comments"] | ||
| H --> I["Done"] | ||
| ``` | ||
|
|
||
| ## Internals | ||
|
|
||
| - `ResolvePrPipeline` orchestrates GitHub fetch, diff capture, analysis, and resolution | ||
| - `CommentAnalyzer` uses `AgentRunner` to classify comments as addressed or not | ||
| - `CommentResolver` posts replies and resolves threads via `GitHubClient` | ||
22 changes: 22 additions & 0 deletions
22
src/cli/resolve-pr-comments/clients/comment-analyzer.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { truncateDiff } from "./comment-analyzer"; | ||
|
|
||
| describe("truncateDiff", () => { | ||
| it("returns original diff when within limit", () => { | ||
| const result = truncateDiff("abc", 10); | ||
|
|
||
| expect(result.diff).toBe("abc"); | ||
| expect(result.truncated).toBe(false); | ||
| expect(result.originalLength).toBe(3); | ||
| }); | ||
|
|
||
| it("truncates and annotates diff when over limit", () => { | ||
| const result = truncateDiff("abcdef", 3); | ||
|
|
||
| expect(result.diff).toContain("abc"); | ||
| expect(result.diff).toContain("diff truncated to 3 characters"); | ||
| expect(result.truncated).toBe(true); | ||
| expect(result.originalLength).toBe(6); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import { AgentRunner } from "~clients/agent-runner"; | ||
| import type { ReviewComment } from "~clients/github-client"; | ||
| import type { Logger } from "~clients/logger"; | ||
|
|
||
| import { ANALYSIS_PROMPT_TEMPLATE, MAX_DIFF_CHARS } from "../constants"; | ||
| import type { AnalysisResult } from "../types/schemas"; | ||
| import { AnalysisResultSchema } from "../types/schemas"; | ||
|
|
||
| type CommentAnalyzerOptions = { | ||
| logger: Logger; | ||
| }; | ||
|
|
||
| type AnalyzeOptions = { | ||
| comments: ReviewComment[]; | ||
| diff: string; | ||
| }; | ||
|
|
||
| export const truncateDiff = ( | ||
| diff: string, | ||
| maxChars: number | ||
| ): { diff: string; truncated: boolean; originalLength: number } => { | ||
| if (diff.length <= maxChars) { | ||
| return { diff, truncated: false, originalLength: diff.length }; | ||
| } | ||
|
|
||
| const truncatedDiff = diff.slice(0, maxChars); | ||
| return { | ||
| diff: `${truncatedDiff}\n\n... [diff truncated to ${maxChars} characters]\n`, | ||
| truncated: true, | ||
| originalLength: diff.length, | ||
| }; | ||
| }; | ||
|
|
||
| /** | ||
| * AI-powered comment analyzer that determines if comments are addressed by a diff. | ||
| * Makes a single API call to analyze all comments together. | ||
| */ | ||
| export class CommentAnalyzer { | ||
| private logger: Logger; | ||
|
|
||
| constructor(options: CommentAnalyzerOptions) { | ||
| this.logger = options.logger; | ||
| } | ||
|
|
||
| async analyze({ comments, diff }: AnalyzeOptions): Promise<AnalysisResult> { | ||
| const commentsJson = JSON.stringify( | ||
| comments.map((c) => ({ | ||
| id: c.id, | ||
| path: c.path, | ||
| line: c.line ?? c.original_line, | ||
| body: c.body, | ||
| user: c.user.login, | ||
| })), | ||
| null, | ||
| 2 | ||
| ); | ||
|
|
||
| const truncatedDiff = truncateDiff(diff, MAX_DIFF_CHARS); | ||
| const prompt = ANALYSIS_PROMPT_TEMPLATE(commentsJson, truncatedDiff.diff); | ||
|
|
||
| if (truncatedDiff.truncated) { | ||
| this.logger.warn("Diff truncated for analysis", { | ||
| originalLength: truncatedDiff.originalLength, | ||
| maxChars: MAX_DIFF_CHARS, | ||
| }); | ||
| } | ||
|
|
||
| const runner = new AgentRunner<AnalysisResult>({ | ||
| name: "comment-analyzer", | ||
| model: "gpt-5-mini", | ||
| tools: [], | ||
| outputType: AnalysisResultSchema, | ||
| instructions: | ||
| "Analyze PR comments to determine if they are addressed by the diff.", | ||
| logger: this.logger, | ||
| stateless: true, | ||
| }); | ||
|
|
||
| this.logger.info("Analyzing comments with AI", { count: comments.length }); | ||
| const result = await runner.run({ prompt }); | ||
|
|
||
| return result.finalOutput as AnalysisResult; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { GitHubClient } from "~clients/github-client"; | ||
| import type { Logger } from "~clients/logger"; | ||
|
|
||
| import type { CommentAnalysis, PrContext } from "../types/schemas"; | ||
|
|
||
| type CommentResolverOptions = { | ||
| logger: Logger; | ||
| }; | ||
|
|
||
| type ResolveOptions = { | ||
| analysis: CommentAnalysis; | ||
| ctx: PrContext; | ||
| dryRun: boolean; | ||
| }; | ||
|
|
||
| /** | ||
| * Handles GitHub API operations for replying to and reacting to PR comments. | ||
| * Uses 👍 reaction to mark addressed comments instead of resolving threads. | ||
| */ | ||
| export class CommentResolver { | ||
| private githubClient: GitHubClient; | ||
| private logger: Logger; | ||
|
|
||
| constructor(options: CommentResolverOptions) { | ||
| this.logger = options.logger; | ||
| this.githubClient = new GitHubClient({ logger: options.logger }); | ||
| } | ||
|
|
||
| /** | ||
| * Get comment IDs that have already been marked with 👍 reaction. | ||
| */ | ||
| async getAlreadyAddressedIds( | ||
| ctx: PrContext, | ||
| commentIds: number[] | ||
| ): Promise<Set<number>> { | ||
| return this.githubClient.getCommentIdsWithReaction(ctx, commentIds, "+1"); | ||
| } | ||
|
|
||
| /** | ||
| * Get comment IDs that were previously marked as uncertain with 👀 reaction. | ||
| */ | ||
| async getPreviouslyUncertainIds( | ||
| ctx: PrContext, | ||
| commentIds: number[] | ||
| ): Promise<Set<number>> { | ||
| return this.githubClient.getCommentIdsWithReaction(ctx, commentIds, "eyes"); | ||
| } | ||
|
|
||
| async resolveComment({ | ||
| analysis, | ||
| ctx, | ||
| dryRun, | ||
| }: ResolveOptions): Promise<boolean> { | ||
| if (analysis.status === "not_addressed") { | ||
| this.logger.debug("Skipping unaddressed comment", { | ||
| commentId: analysis.commentId, | ||
| }); | ||
| return false; | ||
| } | ||
|
|
||
| const replyBody = analysis.suggestedReply; | ||
| const isAddressed = analysis.status === "addressed"; | ||
|
|
||
| if (dryRun) { | ||
| this.logger.info( | ||
| isAddressed | ||
| ? "[DRY RUN] Would reply and react with 👍" | ||
| : "[DRY RUN] Would reply and react with 👀", | ||
| { | ||
| commentId: analysis.commentId, | ||
| status: analysis.status, | ||
| reply: replyBody, | ||
| reasoning: analysis.reasoning, | ||
| } | ||
| ); | ||
| return false; | ||
| } | ||
|
|
||
| await this.githubClient.replyToComment(ctx, analysis.commentId, replyBody); | ||
| this.logger.info("Posted reply", { | ||
| commentId: analysis.commentId, | ||
| status: analysis.status, | ||
| }); | ||
|
|
||
| if (isAddressed) { | ||
| await this.githubClient.reactToComment(ctx, analysis.commentId, "+1"); | ||
| this.logger.info("Added 👍 reaction", { commentId: analysis.commentId }); | ||
| } else { | ||
| // Uncertain - mark with 👀 so we don't re-process unless file changes | ||
| await this.githubClient.reactToComment(ctx, analysis.commentId, "eyes"); | ||
| this.logger.info("Added 👀 reaction", { commentId: analysis.commentId }); | ||
| } | ||
|
|
||
| return isAddressed; | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docs say the tool “reply[s] to and resolve[s] … comments already addressed”, but the implementation replies for
uncertainstatuses too (it only skipsnot_addressed). Either update the docs to mention thatuncertaincomments will receive a clarification reply, or change the resolver to only post replies whenstatus === "addressed".There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated docs to reflect behavior: we now reply to both addressed and uncertain comments (uncertain replies ask for verification) and only mark/resolve comments when status === 'addressed'.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks — updated README and implementation now reply to both addressed and uncertain analyses, and only mark/address comments when status === 'addressed'.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
README and implementation updated to reply for both addressed and uncertain analyses and only mark comments when status === 'addressed' — implemented as described.