Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 2 additions & 2 deletions .github/workflows/ai-review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ on:
agents:
description: 'Number of agents (1-3)'
required: false
default: '2'
default: '3'
type: choice
options:
- '1'
Expand Down Expand Up @@ -63,7 +63,7 @@ jobs:
echo "agents=${{ github.event.inputs.agents }}" >> $GITHUB_OUTPUT
else
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
echo "agents=2" >> $GITHUB_OUTPUT
echo "agents=3" >> $GITHUB_OUTPUT
fi

- name: Run AI Code Review
Expand Down
38 changes: 33 additions & 5 deletions src/ai_reviewer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def cli(verbose: bool) -> None:
@click.option("--output", type=click.Choice(["github", "json", "markdown"]), default="github")
@click.option("--dry-run", is_flag=True, help="Don't post to GitHub")
@click.option(
"--agents", type=int, default=1, help="Number of agents (1-3): 1=comprehensive, 2+=specialized"
"--agents", type=int, default=3, help="Number of agents (1-3): 1=comprehensive, 2+=specialized (default: 3)"
)
@click.option(
"--no-approve", is_flag=True, help="Don't use APPROVE action (auto-enabled in GitHub Actions)"
Expand All @@ -57,6 +57,19 @@ def cli(verbose: bool) -> None:
"--reviewer-name", default="AI Code Reviewer", help="Custom name to display in review header"
)
@click.option("--config", "config_path", type=click.Path(exists=True), help="Config file path")
@click.option(
"--no-cross-review",
"no_cross_review",
is_flag=True,
help="Disable second round where agents validate and rank findings (default: cross-review on when --agents>=2)",
)
@click.option(
"--min-agreement",
"min_agreement",
type=click.FloatRange(0.0, 1.0),
default=2 / 3,
help="Fraction of assessing agents that must mark a finding valid (default: 2/3; with 2 agents, both must agree)",
)
def review_pr(
repo: str,
pr_number: int,
Expand All @@ -66,12 +79,16 @@ def review_pr(
no_approve: bool,
reviewer_name: str,
config_path: str | None,
no_cross_review: bool,
min_agreement: float,
) -> None:
"""Review a GitHub pull request using Cursor AI agent(s).

With --agents=1 (default): Single comprehensive review
With --agents=2: Security + Performance agents
With --agents=3: Security + Performance + Quality agents
With --agents=1: Single comprehensive review
With --agents=2: Security + Performance agents (cross-review on by default)
With --agents=3 (default): Security + Performance + Quality agents (cross-review on by default)
Use --no-cross-review to skip the second round (validate & rank findings).
Use --min-agreement to tune how many agents must validate a finding to keep it (0-1).
"""
asyncio.run(
review_pr_async(
Expand All @@ -83,6 +100,8 @@ def review_pr(
no_approve=no_approve,
reviewer_name=reviewer_name,
config_path=Path(config_path) if config_path else None,
enable_cross_review=not no_cross_review,
min_validation_agreement=min_agreement,
)
)

Expand All @@ -92,10 +111,12 @@ async def review_pr_async(
pr_number: int,
output: str = "github",
dry_run: bool = False,
num_agents: int = 1,
num_agents: int = 3,
no_approve: bool = False,
reviewer_name: str = "AI Code Reviewer",
config_path: Path | None = None,
enable_cross_review: bool = True,
min_validation_agreement: float = 2 / 3,
) -> None:
"""Async implementation of PR review using Cursor Background Agent(s)."""
# Auto-detect GitHub Actions environment - never allow APPROVE there
Expand All @@ -120,6 +141,11 @@ async def review_pr_async(
console.print(
f"[yellow]Using {num_agents} specialized agents: {', '.join(agent_types)} (3-8 min)[/yellow]"
)
if num_agents > 1:
if enable_cross_review:
console.print("[dim]Cross-review enabled: agents will validate and rank findings[/dim]")
else:
console.print("[dim]Cross-review disabled[/dim]")

# Status callback
last_status = [None]
Expand All @@ -144,6 +170,8 @@ def on_status(status: str) -> None:
github_token=config.github.token,
on_status=on_status,
num_agents=num_agents,
enable_cross_review=enable_cross_review,
min_validation_agreement=min_validation_agreement,
)
except Exception as e:
console.print(f"[red]Error:[/red] {e}")
Expand Down
23 changes: 12 additions & 11 deletions src/ai_reviewer/github/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,9 @@ def get_review_action_with_delta(
) -> str:
"""Determine GitHub review action considering the delta.

LGTM-with-comments: REQUEST_CHANGES only for critical findings. When there are
only warnings, suggestions, or nitpicks, returns COMMENT so the author isn't blocked.

Args:
review: Consolidated review
delta: Review delta
Expand All @@ -418,25 +421,25 @@ def get_review_action_with_delta(
Returns:
GitHub review action
"""
# If all issues are resolved, approve (if allowed)
if delta.all_issues_resolved and allow_approve:
return "APPROVE"

# If there are critical issues (new or open), request changes
# But only if allow_approve is True (i.e., not in GitHub Actions)
# GitHub Actions can't approve, and REQUEST_CHANGES blocks merging
# Block merge only when there are critical findings (not warnings/suggestions/nitpicks)
has_critical = any(
f.severity.value == "critical" for f in delta.new_findings + delta.open_findings
)
if has_critical and allow_approve:
return "REQUEST_CHANGES"

# In GitHub Actions or no critical issues, just comment
# No critical: COMMENT (includes only nits/suggestions/warnings — don't block author)
return "COMMENT"

def get_review_action(self, review: ConsolidatedReview, allow_approve: bool = True) -> str:
"""Determine the GitHub review action based on findings.

LGTM-with-comments: REQUEST_CHANGES only for critical findings. When there are
only warnings, suggestions, or nitpicks, returns COMMENT so the author isn't blocked.

Args:
review: Consolidated review
allow_approve: Whether to allow APPROVE/REQUEST_CHANGES actions
Expand All @@ -445,14 +448,12 @@ def get_review_action(self, review: ConsolidatedReview, allow_approve: bool = Tr
Returns:
GitHub review action: "APPROVE", "REQUEST_CHANGES", or "COMMENT"
"""
# REQUEST_CHANGES blocks merging - only use when allow_approve is True
# (i.e., running locally with proper permissions)
if not review.findings and allow_approve:
return "APPROVE"
# Block merge only on critical; warnings/suggestions/nitpicks → COMMENT
if review.has_critical_issues and allow_approve:
return "REQUEST_CHANGES"
elif not review.findings and allow_approve:
return "APPROVE"
else:
return "COMMENT"
return "COMMENT"


def format_review_as_json(review: ConsolidatedReview) -> dict:
Expand Down
28 changes: 26 additions & 2 deletions src/ai_reviewer/github/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,32 @@ async def default_review_handler(repo: str, pr_number: int) -> None:
logger.error("GITHUB_TOKEN not set")
return

try:
cursor_timeout = int(os.environ.get("CURSOR_TIMEOUT", "300"))
except ValueError:
cursor_timeout = 300
logger.warning("CURSOR_TIMEOUT invalid, using default 300")

try:
num_agents = int(os.environ.get("NUM_AGENTS", "3"))
except ValueError:
num_agents = 3
logger.warning("NUM_AGENTS invalid, using default 3")

try:
min_agreement = float(os.environ.get("MIN_VALIDATION_AGREEMENT", str(2 / 3)))
except ValueError:
min_agreement = 2 / 3
logger.warning("MIN_VALIDATION_AGREEMENT invalid, using default 2/3")

cursor_config = CursorConfig(
api_key=cursor_api_key,
base_url=os.environ.get("CURSOR_BASE_URL", "https://api.cursor.com/v0"),
timeout=int(os.environ.get("CURSOR_TIMEOUT", "300")),
timeout=cursor_timeout,
)

enable_cross_review = (
os.environ.get("ENABLE_CROSS_REVIEW", "true").lower() != "false"
)

try:
Expand All @@ -152,7 +174,9 @@ async def default_review_handler(repo: str, pr_number: int) -> None:
pr_number=pr_number,
cursor_config=cursor_config,
github_token=github_token,
num_agents=int(os.environ.get("NUM_AGENTS", "1")),
num_agents=num_agents,
enable_cross_review=enable_cross_review,
min_validation_agreement=min_agreement,
)

if review.all_agents_failed:
Expand Down
Loading