diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml
new file mode 100644
index 00000000..a57b696d
--- /dev/null
+++ b/.github/workflows/pr-review.yml
@@ -0,0 +1,90 @@
+# GitHub Action: Automated PR Review
+# Triggers on pull request events and posts a structured review comment.
+#
+# Usage: Copy this file to your repo's .github/workflows/ directory.
+# Set GITHUB_TOKEN (automatic) or a PAT with repo scope for cross-repo reviews.
+
+name: PR Review Agent
+
+on:
+ pull_request:
+ types: [opened, synchronize]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ review:
+ runs-on: ubuntu-latest
+ name: Automated PR Review
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Fetch PR diff
+ id: diff
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ PR_URL="${{ github.event.pull_request.html_url }}"
+ echo "pr_url=${PR_URL}" >> $GITHUB_OUTPUT
+
+ # Fetch diff via GitHub API
+ curl -s -H "Authorization: token ${GITHUB_TOKEN}" \
+ -H "Accept: application/vnd.github.v3.diff" \
+ "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" \
+ > /tmp/pr.diff
+
+ echo "Diff saved: $(wc -l < /tmp/pr.diff) lines"
+
+ - name: Generate review
+ id: review
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ python3 ${{ github.workspace }}/claude_review.py \
+ --pr "${{ steps.diff.outputs.pr_url }}" \
+ --output /tmp/review.md
+
+ # Store review content for the comment step
+ cat /tmp/review.md >> $GITHUB_OUTPUT
+
+ - name: Post review comment
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ const review = fs.readFileSync('/tmp/review.md', 'utf8');
+
+ // Find existing bot comment to update
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ });
+
+ const botComment = comments.find(c =>
+ c.user.type === 'Bot' && c.body.includes('Automated PR Review')
+ );
+
+ if (botComment) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: botComment.id,
+ body: review,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: review,
+ });
+ }
diff --git a/agents/pr-reviewer/README.md b/agents/pr-reviewer/README.md
new file mode 100644
index 00000000..2c4c68d9
--- /dev/null
+++ b/agents/pr-reviewer/README.md
@@ -0,0 +1,172 @@
+# ๐ค claude-review โ Automated PR Review Agent
+
+A CLI tool and GitHub Action that analyzes GitHub Pull Requests and generates structured Markdown review reports.
+
+> **$150 Bounty Submission** โ [Issue #4](https://github.com/claude-builders-bounty/claude-builders-bounty/issues/4)
+
+## Features
+
+- ๐ **CLI tool** โ Review any GitHub PR from the command line
+- ๐ค **GitHub Action** โ Auto-review PRs on every push
+- ๐ **Structured output** โ Summary, risks, suggestions, confidence score
+- ๐ **Zero dependencies** โ Pure Python standard library
+- ๐ **Security detection** โ Flags common vulnerabilities in diffs
+
+## Quick Start
+
+### CLI Usage
+
+```bash
+# Install (no pip needed โ single file)
+git clone https://github.com/zlplzp123wyt/claude-builders-bounty
+cd claude-builders-bounty
+pip install -e . # or just run python3 claude_review.py directly
+
+# Review a PR
+claude-review --pr https://github.com/psf/requests/pull/7310
+
+# Save to file
+claude-review --pr https://github.com/owner/repo/pull/42 -o review.md
+
+# JSON output
+claude-review --pr https://github.com/owner/repo/pull/42 --json
+
+# Read diff from stdin
+git diff | claude-review --stdin --pr https://github.com/owner/repo/pull/42
+```
+
+### Environment Variables
+
+| Variable | Description |
+|----------|-------------|
+| `GITHUB_TOKEN` | GitHub personal access token (optional, increases rate limits) |
+
+```bash
+export GITHUB_TOKEN=ghp_xxxxxxxxxxxx
+claude-review --pr https://github.com/owner/repo/pull/42
+```
+
+### GitHub Action
+
+Add this workflow to your repository (`.github/workflows/pr-review.yml`):
+
+```yaml
+name: PR Review Agent
+
+on:
+ pull_request:
+ types: [opened, synchronize]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ review:
+ runs-on: ubuntu-latest
+ name: Automated PR Review
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Generate review
+ id: review
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ python3 ${{ github.workspace }}/claude_review.py \
+ --pr "${{ github.event.pull_request.html_url }}" \
+ --output /tmp/review.md
+
+ - name: Post review comment
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ const review = fs.readFileSync('/tmp/review.md', 'utf8');
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner, repo: context.repo.repo,
+ issue_number: context.issue.number,
+ });
+ const botComment = comments.find(c =>
+ c.user.type === 'Bot' && c.body.includes('Automated PR Review')
+ );
+ if (botComment) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner, repo: context.repo.repo,
+ comment_id: botComment.id, body: review,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner, repo: context.repo.repo,
+ issue_number: context.issue.number, body: review,
+ });
+ }
+```
+
+## Output Format
+
+Each review includes:
+
+### ๐ Summary of Changes
+2โ3 sentence overview: PR title, author, files changed, line counts, and purpose.
+
+### โ ๏ธ Identified Risks
+- ๐ Security risks (hardcoded secrets, eval/exec, SQL injection, XSS)
+- ๐ Large PR warnings (>500 lines)
+- ๐งช Missing test coverage
+- ๐ฆ Dependency changes
+- โ๏ธ Configuration/deployment changes
+- ๐๏ธ Removed tests
+- โ ๏ธ TODO/FIXME markers
+
+### ๐ก Improvement Suggestions
+- Debug statement removal
+- Bare except clause warnings
+- Magic number extraction
+- Missing comments on complex code
+- Documentation updates
+
+### ๐ Confidence Score
+- ๐ข **High** โ Small, well-scoped changes
+- ๐ก **Medium** โ Moderate complexity, manual review recommended
+- ๐ด **Low** โ High complexity/risk, thorough review needed
+
+## Sample Output
+
+See [`examples/`](examples/) for real-world review outputs:
+
+- **sample_review_1.md** โ [psf/requests#7310](https://github.com/psf/requests/pull/7310) (DigestAuth FIPS fix)
+- **sample_review_2.md** โ [fastapi/fastapi#15263](https://github.com/fastapi/fastapi/pull/15263) (pygments bump)
+
+## CLI Arguments
+
+| Argument | Description |
+|----------|-------------|
+| `--pr URL` | GitHub PR URL (required) |
+| `-o`, `--output FILE` | Output file path (default: stdout) |
+| `--json` | Output as JSON instead of Markdown |
+| `--stdin` | Read diff from stdin instead of fetching |
+| `--token TOKEN` | GitHub token (or use `GITHUB_TOKEN` env) |
+
+## How It Works
+
+1. **Parse** the GitHub PR URL to extract owner, repo, and PR number
+2. **Fetch** the PR diff and metadata via GitHub API
+3. **Analyze** the diff for security issues, size, test coverage, patterns
+4. **Generate** a structured Markdown report with summary, risks, suggestions, and confidence
+
+## Requirements
+
+- Python 3.8+
+- Internet access (to fetch PRs from GitHub API)
+- No external dependencies
+
+## License
+
+MIT
diff --git a/agents/pr-reviewer/claude_review.py b/agents/pr-reviewer/claude_review.py
new file mode 100644
index 00000000..ea6bcead
--- /dev/null
+++ b/agents/pr-reviewer/claude_review.py
@@ -0,0 +1,433 @@
+#!/usr/bin/env python3
+"""
+claude-review: A CLI tool that reviews GitHub PRs and outputs structured Markdown reports.
+
+Usage:
+ claude-review --pr https://github.com/owner/repo/pull/123
+ claude-review --pr https://github.com/owner/repo/pull/123 --output review.md
+ cat diff.txt | claude-review --stdin --pr https://github.com/owner/repo/pull/123
+"""
+
+import argparse
+import json
+import os
+import re
+import subprocess
+import sys
+import urllib.request
+import urllib.error
+from typing import Optional
+
+
+def parse_pr_url(url: str) -> dict:
+ """Parse a GitHub PR URL into owner, repo, and PR number."""
+ pattern = r"https?://github\.com/([^/]+)/([^/]+)/pull/(\d+)"
+ match = re.match(pattern, url.strip().rstrip("/"))
+ if not match:
+ raise ValueError(f"Invalid PR URL: {url}")
+ return {
+ "owner": match.group(1),
+ "repo": match.group(2),
+ "pr_number": int(match.group(3)),
+ }
+
+
+def fetch_pr_diff(owner: str, repo: str, pr_number: int, token: Optional[str] = None) -> str:
+ """Fetch the diff of a GitHub PR using the GitHub API."""
+ url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}"
+ headers = {
+ "Accept": "application/vnd.github.v3.diff",
+ "User-Agent": "claude-review/1.0",
+ }
+ if token:
+ headers["Authorization"] = f"token {token}"
+
+ req = urllib.request.Request(url, headers=headers)
+ try:
+ with urllib.request.urlopen(req) as response:
+ return response.read().decode("utf-8")
+ except urllib.error.HTTPError as e:
+ if e.code == 404:
+ raise RuntimeError(f"PR not found: {owner}/{repo}#{pr_number}")
+ elif e.code == 403:
+ raise RuntimeError("GitHub API rate limit exceeded. Set GITHUB_TOKEN to increase limits.")
+ raise RuntimeError(f"GitHub API error: {e.code} {e.reason}")
+
+
+def fetch_pr_metadata(owner: str, repo: str, pr_number: int, token: Optional[str] = None) -> dict:
+ """Fetch PR metadata (title, description, author, etc.)."""
+ url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}"
+ headers = {
+ "Accept": "application/vnd.github.v3+json",
+ "User-Agent": "claude-review/1.0",
+ }
+ if token:
+ headers["Authorization"] = f"token {token}"
+
+ req = urllib.request.Request(url, headers=headers)
+ try:
+ with urllib.request.urlopen(req) as response:
+ return json.loads(response.read().decode("utf-8"))
+ except urllib.error.HTTPError:
+ return {}
+
+
+def analyze_diff(diff: str, metadata: dict) -> dict:
+ """
+ Analyze a PR diff and return structured review data.
+ This performs static analysis without requiring an LLM.
+ """
+ if not diff or diff.strip() == "":
+ return {
+ "summary": "Empty diff โ no changes detected.",
+ "risks": [],
+ "improvements": ["Verify that the PR contains actual changes."],
+ "confidence": "Low",
+ }
+
+ lines = diff.split("\n")
+ added_lines = [l for l in lines if l.startswith("+") and not l.startswith("+++")]
+ removed_lines = [l for l in lines if l.startswith("-") and not l.startswith("---")]
+
+ # Parse files changed
+ files_changed = []
+ current_file = None
+ for line in lines:
+ if line.startswith("diff --git"):
+ parts = line.split(" b/")
+ if len(parts) > 1:
+ current_file = parts[1].strip()
+ files_changed.append({"name": current_file, "additions": 0, "deletions": 0})
+ elif line.startswith("+") and not line.startswith("+++") and files_changed:
+ files_changed[-1]["additions"] += 1
+ elif line.startswith("-") and not line.startswith("---") and files_changed:
+ files_changed[-1]["deletions"] += 1
+
+ title = metadata.get("title", "Unknown PR")
+ body = (metadata.get("body") or "")[:500]
+ author = metadata.get("user", {}).get("login", "unknown")
+ total_additions = sum(f["additions"] for f in files_changed)
+ total_deletions = sum(f["deletions"] for f in files_changed)
+
+ # Build summary
+ file_names = [f["name"] for f in files_changed]
+ file_categories = categorize_files(file_names)
+ summary_parts = [
+ f'"{title}" by @{author} modifies {len(files_changed)} file(s) '
+ f"(+{total_additions}/โ{total_deletions} lines)."
+ ]
+ if file_categories:
+ summary_parts.append(f"Changes span: {', '.join(file_categories)}.")
+ if body:
+ clean_body = body.replace("\n", " ").strip()
+ if clean_body:
+ summary_parts.append(f"Purpose: {clean_body[:200]}.")
+
+ # Identify risks
+ risks = identify_risks(diff, files_changed, added_lines, removed_lines)
+
+ # Suggestions
+ improvements = suggest_improvements(diff, files_changed, added_lines, removed_lines)
+
+ # Confidence
+ confidence = compute_confidence(files_changed, total_additions, total_deletions, risks)
+
+ return {
+ "summary": " ".join(summary_parts),
+ "risks": risks,
+ "improvements": improvements,
+ "confidence": confidence,
+ "stats": {
+ "files_changed": len(files_changed),
+ "additions": total_additions,
+ "deletions": total_deletions,
+ "files": file_names[:20],
+ },
+ }
+
+
+def categorize_files(files: list) -> list:
+ """Categorize changed files into groups."""
+ categories = set()
+ for f in files:
+ if any(f.endswith(ext) for ext in [".yml", ".yaml"]):
+ categories.add("CI/CD configuration")
+ elif any(f.endswith(ext) for ext in [".md", ".txt", ".rst"]):
+ categories.add("documentation")
+ elif any(f.endswith(ext) for ext in [".test.js", ".spec.js", ".test.ts", ".spec.ts", ".test.py", "_test.go"]):
+ categories.add("tests")
+ elif any(f.endswith(ext) for ext in [".css", ".scss", ".html", ".jsx", ".tsx", ".svelte", ".vue"]):
+ categories.add("frontend/UI")
+ elif any(f.endswith(ext) for ext in [".py", ".js", ".ts", ".go", ".rs", ".java", ".rb"]):
+ categories.add("source code")
+ elif any(f.endswith(ext) for ext in [".json", ".toml", ".cfg", ".ini"]):
+ categories.add("configuration")
+ elif any(f.endswith(ext) for ext in [".sh", ".bash"]):
+ categories.add("scripts")
+ elif "dockerfile" in f.lower() or "docker-compose" in f.lower():
+ categories.add("Docker/containerization")
+ else:
+ categories.add("other")
+ return sorted(categories)
+
+
+def identify_risks(diff: str, files: list, added: list, removed: list) -> list:
+ """Identify potential risks in the diff."""
+ risks = []
+ added_text = "\n".join(added)
+ removed_text = "\n".join(removed)
+
+ # Security risks
+ security_patterns = [
+ (r"(?i)(password|secret|api_key|token)\s*=\s*['\"]", "Hardcoded secrets/credentials detected"),
+ (r"(?i)eval\s*\(", "Use of eval() โ potential code injection risk"),
+ (r"(?i)exec\s*\(", "Use of exec() โ potential code execution risk"),
+ (r"(?i)subprocess\.call\(.*shell\s*=\s*True", "Shell injection risk: subprocess with shell=True"),
+ (r"(?i)innerHTML\s*=", "Direct innerHTML assignment โ XSS risk"),
+ (r"(?i)dangerouslySetInnerHTML", "React dangerouslySetInnerHTML usage โ XSS risk"),
+ (r"(?i)disable.*ssl|verify\s*=\s*False", "SSL verification disabled"),
+ (r"(?i)(?:SELECT|INSERT|UPDATE|DELETE).*%s|\.format\(|f\".*SELECT", "Possible SQL injection vulnerability"),
+ ]
+ for pattern, message in security_patterns:
+ if re.search(pattern, added_text):
+ risks.append(f"๐ **Security**: {message}")
+
+ # Size risks
+ if len(added) + len(removed) > 500:
+ risks.append(f"๐ **Large PR**: {len(added)} additions and {len(removed)} deletions across {len(files)} files โ consider splitting into smaller PRs")
+
+ # Test coverage
+ test_files = [f for f in files if any(t in f["name"].lower() for t in ["test", "spec"])]
+ source_files = [f for f in files if f not in test_files]
+ if source_files and not test_files:
+ risks.append("๐งช **No tests**: Source code changed but no test files updated โ potential regression risk")
+
+ # Dependency changes
+ dep_files = [f for f in files if any(d in f["name"] for d in ["package.json", "requirements.txt", "Pipfile", "go.mod", "Cargo.toml", "Gemfile", "pom.xml"])]
+ if dep_files:
+ risks.append(f"๐ฆ **Dependency change**: {', '.join(f['name'] for f in dep_files)} modified โ review for breaking changes or vulnerabilities")
+
+ # Config/deployment changes
+ config_files = [f for f in files if any(c in f["name"].lower() for c in [".env", "config", "settings", "deploy", "kubernetes", "helm"])]
+ if config_files:
+ risks.append(f"โ๏ธ **Config/Deploy change**: {', '.join(f['name'] for f in config_files)} โ verify environment-specific values")
+
+ # Deleted tests
+ deleted_tests = [l for l in removed if re.search(r"(?i)(def test_|it\(|describe\(|@Test)", l)]
+ if deleted_tests:
+ risks.append(f"๐๏ธ **Test removal**: {len(deleted_tests)} test function(s) removed โ ensure intentional")
+
+ # TODO/FIXME/HACK
+ todos = [l for l in added if re.search(r"(?i)(TODO|FIXME|HACK|XXX|WORKAROUND)", l)]
+ if todos:
+ risks.append(f"โ ๏ธ **Technical debt**: {len(todos)} TODO/FIXME/HACK marker(s) added")
+
+ # Catch-all for small PR with no detected risks
+ if not risks:
+ risks.append("โ
No significant risks detected")
+
+ return risks
+
+
+def suggest_improvements(diff: str, files: list, added: list, removed: list) -> list:
+ """Suggest improvements based on the diff."""
+ suggestions = []
+ added_text = "\n".join(added)
+
+ # Logging
+ print_stmts = len(re.findall(r"(?i)(console\.log|print\(|System\.out\.print|fmt\.Print|puts )", added_text))
+ if print_stmts > 0:
+ suggestions.append(f"๐งน Remove or replace {print_stmts} debug print/log statement(s) with proper logging")
+
+ # Error handling
+ bare_except = len(re.findall(r"except\s*:", added_text))
+ if bare_except > 0:
+ suggestions.append("๐ฏ Use specific exception types instead of bare `except:` clauses")
+
+ # Magic numbers
+ magic = re.findall(r"(? 50:
+ has_comments = any(l.strip().startswith(("#", "//", "*", "/*", '"""', "'''")) for l in added)
+ if not has_comments:
+ suggestions.append("๐ Add comments to explain complex logic in large changes")
+
+ # Consistency
+ if "TODO" in added_text or "FIXME" in added_text:
+ suggestions.append("๐ Address or track TODO/FIXME items before merging")
+
+ # Documentation
+ doc_files = [f for f in files if any(d in f["name"] for d in [".md", ".rst", "README"])]
+ source_files = [f for f in files if f not in doc_files]
+ if source_files and not doc_files and len(added) > 20:
+ suggestions.append("๐ Consider updating documentation to reflect code changes")
+
+ # Catch-all
+ if not suggestions:
+ suggestions.append("๐ Code changes look clean โ no specific improvements suggested")
+
+ return suggestions
+
+
+def compute_confidence(files: list, additions: int, deletions: int, risks: list) -> str:
+ """Compute review confidence score."""
+ score = 100
+
+ # Penalize large changes
+ total = additions + deletions
+ if total > 1000:
+ score -= 40
+ elif total > 500:
+ score -= 25
+ elif total > 200:
+ score -= 15
+
+ # Penalize many files
+ if len(files) > 20:
+ score -= 30
+ elif len(files) > 10:
+ score -= 15
+
+ # Penalize security risks
+ security_risks = [r for r in risks if "๐" in r]
+ score -= len(security_risks) * 25
+
+ # Penalize other significant risks
+ other_risks = [r for r in risks if r != "โ
No significant risks detected"]
+ score -= len(other_risks) * 5
+
+ if score >= 75:
+ return "High"
+ elif score >= 45:
+ return "Medium"
+ else:
+ return "Low"
+
+
+def format_markdown(review: dict, pr_url: str) -> str:
+ """Format the review as structured Markdown."""
+ confidence_emoji = {"High": "๐ข", "Medium": "๐ก", "Low": "๐ด"}
+ emoji = confidence_emoji.get(review["confidence"], "โช")
+
+ lines = [
+ "## ๐ค Automated PR Review",
+ "",
+ "### ๐ Summary of Changes",
+ "",
+ review["summary"],
+ "",
+ "### โ ๏ธ Identified Risks",
+ "",
+ ]
+
+ for risk in review["risks"]:
+ lines.append(f"- {risk}")
+
+ lines.extend([
+ "",
+ "### ๐ก Improvement Suggestions",
+ "",
+ ])
+
+ for suggestion in review["improvements"]:
+ lines.append(f"- {suggestion}")
+
+ lines.extend([
+ "",
+ "### ๐ Confidence Score",
+ "",
+ f"{emoji} **{review['confidence']}** โ ",
+ ])
+
+ if review["confidence"] == "High":
+ lines.append("Changes are well-scoped and reviewable.")
+ elif review["confidence"] == "Medium":
+ lines.append("Moderate complexity โ manual review recommended for flagged items.")
+ else:
+ lines.append("High complexity or risk โ thorough manual review strongly recommended.")
+
+ if "stats" in review:
+ stats = review["stats"]
+ lines.extend([
+ "",
+ "",
+ "๐ Diff Statistics
",
+ "",
+ f"- **Files changed:** {stats['files_changed']}",
+ f"- **Lines added:** {stats['additions']}",
+ f"- **Lines removed:** {stats['deletions']}",
+ f"- **Files:** {', '.join(stats['files'][:10])}{'...' if len(stats['files']) > 10 else ''}",
+ "",
+ " ",
+ ])
+
+ lines.extend([
+ "",
+ "---",
+ f"*Generated by [claude-review](https://github.com/zlplzp123wyt/claude-builders-bounty) โข [PR Link]({pr_url})*",
+ ])
+
+ return "\n".join(lines)
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Review GitHub PRs and output structured Markdown reports",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ claude-review --pr https://github.com/facebook/react/pull/1000
+ claude-review --pr https://github.com/vercel/next.js/pull/5000 --output review.md
+ claude-review --pr https://github.com/owner/repo/pull/42 --json
+ """,
+ )
+ parser.add_argument("--pr", required=True, help="GitHub PR URL")
+ parser.add_argument("--output", "-o", help="Output file path (default: stdout)")
+ parser.add_argument("--json", action="store_true", help="Output as JSON instead of Markdown")
+ parser.add_argument("--stdin", action="store_true", help="Read diff from stdin instead of fetching from GitHub")
+ parser.add_argument("--token", help="GitHub token (or set GITHUB_TOKEN env var)")
+
+ args = parser.parse_args()
+
+ # Validate PR URL
+ try:
+ pr_info = parse_pr_url(args.pr)
+ except ValueError as e:
+ print(f"Error: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ token = args.token or os.environ.get("GITHUB_TOKEN")
+
+ # Fetch diff
+ if args.stdin:
+ diff = sys.stdin.read()
+ else:
+ print(f"Fetching PR: {pr_info['owner']}/{pr_info['repo']}#{pr_info['pr_number']}...", file=sys.stderr)
+ diff = fetch_pr_diff(pr_info["owner"], pr_info["repo"], pr_info["pr_number"], token)
+
+ # Fetch metadata
+ metadata = fetch_pr_metadata(pr_info["owner"], pr_info["repo"], pr_info["pr_number"], token)
+
+ # Analyze
+ print("Analyzing changes...", file=sys.stderr)
+ review = analyze_diff(diff, metadata)
+
+ # Output
+ if args.json:
+ output = json.dumps(review, indent=2, ensure_ascii=False)
+ else:
+ output = format_markdown(review, args.pr)
+
+ if args.output:
+ with open(args.output, "w") as f:
+ f.write(output)
+ print(f"Review written to {args.output}", file=sys.stderr)
+ else:
+ print(output)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/agents/pr-reviewer/examples/sample_review_1.md b/agents/pr-reviewer/examples/sample_review_1.md
new file mode 100644
index 00000000..93ef75c3
--- /dev/null
+++ b/agents/pr-reviewer/examples/sample_review_1.md
@@ -0,0 +1,31 @@
+## ๐ค Automated PR Review
+
+### ๐ Summary of Changes
+
+"Move DigestAuth hash algorithms to use usedforsecurity=False" by @nateprewitt modifies 1 file(s) (+5/โ5 lines). Changes span: source code. Purpose: This PR moves the DigestAuth hash algorithms to use `usedforsecurity=False`. This is to both avoid unnecessarily breaking DigestAuth on FIPS enabled systems and to make it clearer to security reporter.
+
+### โ ๏ธ Identified Risks
+
+- ๐งช **No tests**: Source code changed but no test files updated โ potential regression risk
+
+### ๐ก Improvement Suggestions
+
+- ๐ Code changes look clean โ no specific improvements suggested
+
+### ๐ Confidence Score
+
+๐ข **High** โ
+Changes are well-scoped and reviewable.
+
+
+๐ Diff Statistics
+
+- **Files changed:** 1
+- **Lines added:** 5
+- **Lines removed:** 5
+- **Files:** src/requests/auth.py
+
+
+
+---
+*Generated by [claude-review](https://github.com/zlplzp123wyt/claude-builders-bounty) โข [PR Link](https://github.com/psf/requests/pull/7310)*
\ No newline at end of file
diff --git a/agents/pr-reviewer/examples/sample_review_2.md b/agents/pr-reviewer/examples/sample_review_2.md
new file mode 100644
index 00000000..bf51b611
--- /dev/null
+++ b/agents/pr-reviewer/examples/sample_review_2.md
@@ -0,0 +1,31 @@
+## ๐ค Automated PR Review
+
+### ๐ Summary of Changes
+
+"โฌ Bump pygments from 2.19.2 to 2.20.0" by @dependabot[bot] modifies 1 file(s) (+3/โ3 lines). Changes span: other. Purpose: Bumps [pygments](https://github.com/pygments/pygments) from 2.19.2 to 2.20.0. Release notes
Sourced from pyg.
+
+### โ ๏ธ Identified Risks
+
+- ๐งช **No tests**: Source code changed but no test files updated โ potential regression risk
+
+### ๐ก Improvement Suggestions
+
+- ๐ข Consider extracting magic numbers into named constants for readability
+
+### ๐ Confidence Score
+
+๐ข **High** โ
+Changes are well-scoped and reviewable.
+
+
+๐ Diff Statistics
+
+- **Files changed:** 1
+- **Lines added:** 3
+- **Lines removed:** 3
+- **Files:** uv.lock
+
+
+
+---
+*Generated by [claude-review](https://github.com/zlplzp123wyt/claude-builders-bounty) โข [PR Link](https://github.com/fastapi/fastapi/pull/15263)*
\ No newline at end of file
diff --git a/agents/pr-reviewer/requirements.txt b/agents/pr-reviewer/requirements.txt
new file mode 100644
index 00000000..d033758e
--- /dev/null
+++ b/agents/pr-reviewer/requirements.txt
@@ -0,0 +1 @@
+# No external dependencies required โ uses Python standard library only
diff --git a/agents/pr-reviewer/setup.py b/agents/pr-reviewer/setup.py
new file mode 100644
index 00000000..8f0f4c2e
--- /dev/null
+++ b/agents/pr-reviewer/setup.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python3
+"""Setup script for claude-review."""
+from setuptools import setup
+
+setup(
+ name="claude-review",
+ version="1.0.0",
+ description="CLI tool that reviews GitHub PRs and outputs structured Markdown reports",
+ author="claude-review",
+ py_modules=["claude_review"],
+ entry_points={
+ "console_scripts": [
+ "claude-review=claude_review:main",
+ ],
+ },
+ python_requires=">=3.8",
+)