diff --git a/.agent/scripts/quality-loop-helper.sh b/.agent/scripts/quality-loop-helper.sh index 348978d6..2500e32e 100755 --- a/.agent/scripts/quality-loop-helper.sh +++ b/.agent/scripts/quality-loop-helper.sh @@ -56,6 +56,10 @@ readonly FAST_SERVICES="codefactor|version|framework" readonly MEDIUM_SERVICES="sonarcloud|codacy|qlty|code-review-monitoring" readonly SLOW_SERVICES="coderabbit|coderabbitai" +# AI code reviewers (regex pattern for jq test() - anchored to prevent false positives) +# Supported: CodeRabbit, Gemini Code Assist, Augment Code, GitHub Copilot +readonly AI_REVIEWERS="^coderabbit|^gemini-code-assist\\[bot\\]$|^augment-code\\[bot\\]$|^augmentcode\\[bot\\]$|^copilot\\[bot\\]$" + # Timing constants (seconds) readonly WAIT_FAST=10 readonly WAIT_MEDIUM=60 @@ -604,14 +608,15 @@ check_pr_status() { check_count=$(echo "$pr_info" | jq '.statusCheckRollup | length') if [[ "$check_count" -gt 0 ]]; then - local pending_count failed_count - pending_count=$(echo "$pr_info" | jq '[.statusCheckRollup[] | select(.status == "PENDING" or .status == "IN_PROGRESS")] | length') - failed_count=$(echo "$pr_info" | jq '[.statusCheckRollup[] | select(.conclusion == "FAILURE")] | length') + local pending_count failed_count action_required_count + pending_count=$(printf '%s' "$pr_info" | jq '[.statusCheckRollup[] | select(.status == "PENDING" or .status == "IN_PROGRESS")] | length') + failed_count=$(printf '%s' "$pr_info" | jq '[.statusCheckRollup[] | select(.conclusion == "FAILURE")] | length') + action_required_count=$(printf '%s' "$pr_info" | jq '[.statusCheckRollup[] | select(.conclusion == "ACTION_REQUIRED")] | length') - print_info " CI Checks: $check_count total, $pending_count pending, $failed_count failed" + print_info " CI Checks: $check_count total, $pending_count pending, $failed_count failed, $action_required_count action required" [[ "$pending_count" -gt 0 ]] && checks_pending=true - [[ "$failed_count" -gt 0 ]] && checks_failed=true + [[ "$failed_count" -gt 0 || "$action_required_count" -gt 0 ]] && checks_failed=true fi # Determine overall status @@ -640,13 +645,21 @@ get_pr_feedback() { print_step "Getting PR feedback..." - # Get CodeRabbit comments - local coderabbit_comments - coderabbit_comments=$(gh api "repos/{owner}/{repo}/pulls/${pr_number}/comments" --jq '.[] | select(.user.login | contains("coderabbit")) | .body' 2>/dev/null | head -10 || echo "") + # Get AI reviewer comments (CodeRabbit, Gemini Code Assist, Augment Code, Copilot) + local ai_review_comments api_response + api_response=$(gh api "repos/{owner}/{repo}/pulls/${pr_number}/comments" 2>/dev/null) - if [[ -n "$coderabbit_comments" ]]; then - print_info "CodeRabbit feedback found" - echo "$coderabbit_comments" + if [[ -z "$api_response" ]]; then + print_warning "Failed to fetch PR comments from GitHub API" + else + ai_review_comments=$(printf '%s' "$api_response" | jq -r --arg bots "$AI_REVIEWERS" \ + '.[] | select(.user.login | test($bots; "i")) | "\(.user.login): \(.body)"' \ + 2>/dev/null | head -20) + + if [[ -n "$ai_review_comments" ]]; then + print_info "AI reviewer feedback found" + echo "$ai_review_comments" + fi fi # Get check run annotations @@ -685,10 +698,16 @@ check_and_trigger_review() { return 1 fi - # Get last CodeRabbit review time - local last_review_time - last_review_time=$(gh api "repos/{owner}/{repo}/pulls/${pr_number}/reviews" \ - --jq '[.[] | select(.user.login | contains("coderabbit"))] | sort_by(.submitted_at) | last | .submitted_at // ""' 2>/dev/null || echo "") + # Get last AI reviewer review time (any supported reviewer) + local last_review_time api_response + api_response=$(gh api "repos/{owner}/{repo}/pulls/${pr_number}/reviews" 2>/dev/null) + + if [[ -n "$api_response" ]]; then + last_review_time=$(printf '%s' "$api_response" | jq -r --arg bots "$AI_REVIEWERS" \ + '[.[] | select(.user.login | test($bots; "i"))] | sort_by(.submitted_at) | last | .submitted_at // ""' 2>/dev/null) + else + last_review_time="" + fi # Convert times to epoch for comparison local now_epoch last_push_epoch last_review_epoch @@ -728,6 +747,52 @@ check_and_trigger_review() { return 1 } +# Check for unresolved review threads on a PR using GraphQL +# Arguments: $1 - PR number +# Returns: 0 if no unresolved threads, 1 if unresolved threads exist, 2 on API error +# Output: Warning message if unresolved threads found +check_unresolved_review_comments() { + local pr_number="$1" + + local repo_owner repo_name api_response unresolved_count + repo_owner=$(gh repo view --json owner -q '.owner.login' 2>/dev/null || echo "") + repo_name=$(gh repo view --json name -q '.name' 2>/dev/null || echo "") + + if [[ -z "$repo_owner" || -z "$repo_name" ]]; then + print_error "Failed to resolve repo owner/name - cannot verify review status" + return 2 + fi + + # shellcheck disable=SC2016 # GraphQL variables, not shell - single quotes intentional + api_response=$(gh api graphql -f query=' + query($owner:String!, $repo:String!, $number:Int!) { + repository(owner:$owner, name:$repo) { + pullRequest(number:$number) { + reviewThreads(first:100) { nodes { isResolved } } + } + } + }' -f owner="$repo_owner" -f repo="$repo_name" -F number="$pr_number" 2>/dev/null) + + if [[ -z "$api_response" ]]; then + print_error "Failed to fetch PR review threads from GitHub API - cannot verify review status" + return 2 + fi + + unresolved_count=$(printf '%s' "$api_response" | jq -r \ + '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length' 2>/dev/null) + + if ! [[ "$unresolved_count" =~ ^[0-9]+$ ]]; then + print_error "Failed to parse unresolved thread count - cannot proceed safely" + return 2 + fi + + if [[ "$unresolved_count" -gt 0 ]]; then + print_warning "Found $unresolved_count unresolved review threads" + return 1 + fi + return 0 +} + # Monitor PR until approved or merged # Arguments: --pr NUMBER, --wait-for-ci, --max-iterations N, --auto-trigger-review # Returns: 0 on approval/merge, 1 if max iterations reached @@ -802,10 +867,23 @@ pr_review_loop() { return 0 ;; READY) - print_success "PR is approved and ready to merge!" - rm -f "$STATE_FILE" - echo "PR_APPROVED" - return 0 + # Check for unresolved AI review comments before declaring ready + local unresolved_check_result + check_unresolved_review_comments "$pr_number" + unresolved_check_result=$? + + if [[ $unresolved_check_result -eq 2 ]]; then + print_warning "Could not verify AI review status (API error) - proceeding with caution" + elif [[ $unresolved_check_result -eq 1 ]]; then + print_warning "PR approved but has unresolved AI review comments" + get_pr_feedback "$pr_number" + print_info "Address the AI reviewer feedback and push updates." + else + print_success "PR is approved and ready to merge!" + rm -f "$STATE_FILE" + echo "PR_APPROVED" + return 0 + fi ;; PENDING) # Get pending checks and calculate adaptive wait diff --git a/.agent/workflows/pr.md b/.agent/workflows/pr.md index 3a6664da..1bb0c067 100644 --- a/.agent/workflows/pr.md +++ b/.agent/workflows/pr.md @@ -97,6 +97,19 @@ Calls remote quality services via APIs: - Codacy - Code quality analysis - SonarCloud - Security and maintainability +### Supported AI Code Reviewers + +The PR loop monitors comments from multiple AI code review services: + +| Reviewer | Bot Username Pattern | Purpose | +|----------|---------------------|---------| +| CodeRabbit | `coderabbit*` | AI-powered code review with suggestions | +| Gemini Code Assist | `gemini-code-assist[bot]` | Google's AI code review | +| Augment Code | `augment-code[bot]`, `augmentcode[bot]` | AI-powered code review and improvement | +| GitHub Copilot | `copilot[bot]` | GitHub's AI assistant | + +The `/pr-loop` command automatically detects and surfaces comments from all these reviewers, ensuring no feedback is missed regardless of which AI services are configured on the repository. + ### 3. Standards Compliance (`/code-standards`) Checks against our documented quality standards: