diff --git a/.agents/scripts/commands/pulse.md b/.agents/scripts/commands/pulse.md index 8983fdb4c..0bf0399f6 100644 --- a/.agents/scripts/commands/pulse.md +++ b/.agents/scripts/commands/pulse.md @@ -115,7 +115,9 @@ Then skip to the next PR. The next pulse cycle will retry the permission check **For maintainer PRs (admin/maintain/write permission):** -- **Green CI + no blocking reviews** → merge: `gh pr merge --repo --squash`. If the PR resolves an issue, the issue should be closed with a comment linking to the merged PR. +- **Green CI + at least one review posted + no blocking reviews** → merge: `gh pr merge --repo --squash`. If the PR resolves an issue, the issue should be closed with a comment linking to the merged PR. + - **CRITICAL (t2839):** Always verify the formal review count first: `gh pr view --repo --json reviews --jq '.reviews | length'`. If count > 0, the review gate passes. If `review-bot-gate-helper.sh check ` is available, use it as an additional bot-activity signal — `PASS` from the bot gate is sufficient on its own. However, `WAITING` only means "no known bot activity" — it does NOT mean zero reviews. When `WAITING` is returned, check the review count explicitly (the `gh pr view` command above); if count > 0, proceed to merge. `SKIP` means the PR has a `skip-review-gate` label — it bypasses the bot gate only, NOT the review count requirement. Skip the PR only when both the bot gate is not `PASS` AND the formal review count is 0. +- **Green CI + zero reviews** → skip this cycle. Zero reviews means "not yet reviewed", NOT "clean to merge". Review bots typically post within 2-5 minutes. The next pulse will pick it up once a review exists. - **Failing CI or changes requested** → dispatch a worker to fix it (counts against worker slots) **For all PRs (regardless of author):** diff --git a/.agents/scripts/supervisor-archived/deploy.sh b/.agents/scripts/supervisor-archived/deploy.sh index a5f00df41..02b69ea61 100755 --- a/.agents/scripts/supervisor-archived/deploy.sh +++ b/.agents/scripts/supervisor-archived/deploy.sh @@ -342,15 +342,64 @@ cmd_pr_lifecycle() { pr_number_fastpath="${parsed_fastpath##*|}" if [[ -n "$pr_number_fastpath" && -n "$repo_slug_fastpath" ]]; then + # t2839: Check that at least one review exists before fast-path merge. + # Zero reviews means "not yet reviewed", not "clean to merge". + # Always count formal reviews (human or bot) via gh API as the + # authoritative source. The bot gate is an additional signal only. + local review_gate_result="UNKNOWN" + local review_count_fastpath="" + review_count_fastpath=$(gh pr view "$pr_number_fastpath" --repo "$repo_slug_fastpath" \ + --json reviews --jq '.reviews | length' 2>>"${SUPERVISOR_LOG:-/dev/null}" || echo "") + + # Optional bot-signal gate (only PASS is sufficient on its own) + local bot_gate_result="WAITING" + local review_bot_gate_script + review_bot_gate_script="$(dirname "$(dirname "${BASH_SOURCE[0]}")")/review-bot-gate-helper.sh" + if [[ -x "$review_bot_gate_script" ]]; then + bot_gate_result=$("$review_bot_gate_script" check "$pr_number_fastpath" "$repo_slug_fastpath" 2>>"${SUPERVISOR_LOG:-/dev/null}") || bot_gate_result="WAITING" + fi + + # Determine review gate result: + # - Bot gate PASS = bot confirmed reviews exist → sufficient + # - Bot gate SKIP = label-driven bypass of bot check, NOT proof of reviews + # SKIP only skips the bot gate; the review count check still applies + # - review_count > 0 = at least one formal review exists → sufficient + # - Otherwise → WAITING (no reviews yet) + if [[ "$bot_gate_result" == "PASS" ]]; then + review_gate_result="PASS" + elif [[ "$review_count_fastpath" =~ ^[0-9]+$ && "$review_count_fastpath" -gt 0 ]]; then + review_gate_result="PASS" + else + review_gate_result="WAITING" + fi + + if [[ "$review_gate_result" == "WAITING" ]]; then + log_info "Fast-path blocked: no reviews posted yet for $task_id — waiting for review before merge (t2839)" + # Stay in pr_review state; next pulse will re-check + local stage_end + stage_end=$(date +%s) + stage_timings="${stage_timings}pr_review:$((stage_end - stage_start))s(no_reviews)," + record_lifecycle_timing "$task_id" "$stage_timings" 2>>"${SUPERVISOR_LOG:-/dev/null}" || true + return 0 + fi + local threads_json_fastpath threads_json_fastpath=$(check_review_threads "$repo_slug_fastpath" "$pr_number_fastpath" 2>/dev/null || echo "[]") local thread_count_fastpath thread_count_fastpath=$(echo "$threads_json_fastpath" | jq 'length' 2>/dev/null || echo "0") if [[ "$thread_count_fastpath" -eq 0 ]]; then - log_info "Fast-path: CI green + zero review threads - skipping review_triage, going directly to merge${merge_note}" + log_info "Fast-path: CI green + reviews posted + zero unresolved threads - skipping review_triage, going directly to merge${merge_note}" if [[ "$dry_run" == "false" ]]; then - db "$SUPERVISOR_DB" "UPDATE tasks SET triage_result = '{\"action\":\"merge\",\"threads\":0,\"fast_path\":true,\"sonarcloud_unstable\":$(if [[ "$pr_status" == "unstable_sonarcloud" ]]; then echo "true"; else echo "false"; fi)}' WHERE id = '$escaped_id';" + # Use parameterized JSON construction to prevent SQL injection (Gemini review feedback) + local sonarcloud_flag="false" + if [[ "$pr_status" == "unstable_sonarcloud" ]]; then + sonarcloud_flag="true" + fi + local triage_json + triage_json=$(jq -n --arg gate "$review_gate_result" --argjson sc "$sonarcloud_flag" \ + '{"action":"merge","threads":0,"fast_path":true,"review_gate":$gate,"sonarcloud_unstable":$sc}') + db "$SUPERVISOR_DB" "UPDATE tasks SET triage_result = '$(echo "$triage_json" | sed "s/'/''/g")' WHERE id = '$escaped_id';" cmd_transition "$task_id" "merging" 2>>"$SUPERVISOR_LOG" || true fi tstatus="merging"