diff --git a/.opencode/agents/ca-continuous-pr-reviewer.md b/.opencode/agents/ca-continuous-pr-reviewer.md index 9d55b0997..e196fe3eb 100644 --- a/.opencode/agents/ca-continuous-pr-reviewer.md +++ b/.opencode/agents/ca-continuous-pr-reviewer.md @@ -196,6 +196,22 @@ LOOP FOREVER: needs_review = True review_reason = "stale-review" + # Case 4: Approved but not merged (stuck) + if has_approval and not pr.merged: + # Find when it was approved + approval_time = None + for review in reviews: + if review.state == "APPROVED": + if not approval_time or review.submitted_at > approval_time: + approval_time = review.submitted_at + + if approval_time: + age_since_approval = (now - approval_time).total_hours() + if age_since_approval > 1: # Approved for >1 hour but not merged + needs_review = True + review_reason = "approved-but-stuck" + # This will dispatch a reviewer to check why it's not merging + # Skip if CI is clearly failing (let implementor fix first) # Check recent comments for CI status indicators if needs_review: @@ -245,18 +261,39 @@ LOOP FOREVER: timeout=30000) # Prepare prompt with review focus - prompt = f"""You are a PR reviewer focusing on code quality. + if item["reason"] == "approved-but-stuck": + # Special prompt for stuck PRs + prompt = f"""You are a PR reviewer investigating why an APPROVED PR has not merged. PR to review: #{pr.number} Repository: {owner}/{repo} - Review reason: {item["reason"]} - REVIEW FOCUS for this session: {', '.join(review_focus)} - While you should check all standard items (spec compliance, tests, etc.), - pay SPECIAL ATTENTION to the focus areas above. + This PR has been APPROVED but has not merged for over 1 hour. + + CRITICAL: If this is a bot PR (contains "Automated by CleverAgents Bot" in description), + it should merge with just 1 approval. Check: + 1. Are all CI checks passing? + 2. Is there at least 1 approval? + 3. Are there any merge conflicts? + 4. Is the PR blocked by rejected reviews? + + If all conditions are met, this may be a stuck PR that needs investigation. Reference summary: {ref_summary} """ + else: + prompt = f"""You are a PR reviewer focusing on code quality. + + PR to review: #{pr.number} + Repository: {owner}/{repo} + Review reason: {item["reason"]} + + REVIEW FOCUS for this session: {', '.join(review_focus)} + While you should check all standard items (spec compliance, tests, etc.), + pay SPECIAL ATTENTION to the focus areas above. + + Reference summary: {ref_summary} + """ # Dispatch reviewer bash("curl -s -X POST ${SERVER}/session/${SESSION_ID}/prompt_async \ diff --git a/.opencode/agents/ca-issue-worker.md b/.opencode/agents/ca-issue-worker.md index 9a581d17a..61d803318 100644 --- a/.opencode/agents/ca-issue-worker.md +++ b/.opencode/agents/ca-issue-worker.md @@ -199,6 +199,29 @@ elif work_type == "merge-conflicts": "---\n**Automated by CleverAgents Bot**\nSupervisor: Implementation | Agent: ca-issue-worker") elif work_type == "ready-to-merge": + # Helper function to check approvals + def has_required_approvals(): + """ + Check if PR has required approvals. + CRITICAL: Bot PRs only need 1 approval from anyone (including other bots). + Human PRs need 2 approvals per CONTRIBUTING.md. + """ + pr_data = forgejo_get_pull_request_by_index(owner, repo, pr_number) + reviews = forgejo_list_pull_reviews(owner, repo, pr_number) + + # Count approvals + approvals = [r for r in reviews if r.state == "APPROVED"] + + # Check if this is a bot PR + is_bot_pr = "Automated by CleverAgents Bot" in pr_data.body + + if is_bot_pr: + # Bot PRs can merge with 1 approval from anyone + return len(approvals) >= 1 + else: + # Human PRs need 2 approvals + return len(approvals) >= 2 + # Verify all checks pass if all_checks_passing() and has_required_approvals(): # Merge the PR @@ -686,11 +709,41 @@ def handle_merge_conflicts(): ```python def merge_pr(): - # Verify one more time + """Merge PR if conditions are met""" + # Check if PR has required approvals + def has_required_approvals(): + """ + Check if PR has required approvals. + CRITICAL: Bot PRs only need 1 approval from anyone (including other bots). + Human PRs need 2 approvals per CONTRIBUTING.md. + """ + pr_data = forgejo_get_pull_request_by_index(owner, repo, pr_number) + reviews = forgejo_list_pull_reviews(owner, repo, pr_number) + + # Count approvals + approvals = [r for r in reviews if r.state == "APPROVED"] + + # Check if this is a bot PR + is_bot_pr = "Automated by CleverAgents Bot" in pr_data.body + + if is_bot_pr: + # Bot PRs can merge with 1 approval from anyone + return len(approvals) >= 1 + else: + # Human PRs need 2 approvals + return len(approvals) >= 2 + + # For bot PRs: 1 approval + passing CI = ready to merge + if not has_required_approvals(): + print("[WAITING] PR needs approval before merge") + return False + if not all_checks_passing(): + print("[WAITING] PR has failing checks") return False - # Merge the PR + # All conditions met - merge it! + print("[MERGING] All conditions met - merging PR") result = forgejo_merge_pull_request(owner, repo, pr_number, style="squash", delete_branch_after_merge=True) diff --git a/.opencode/agents/ca-project-bootstrapper.md b/.opencode/agents/ca-project-bootstrapper.md index 805c8426b..a76708164 100644 --- a/.opencode/agents/ca-project-bootstrapper.md +++ b/.opencode/agents/ca-project-bootstrapper.md @@ -172,12 +172,18 @@ checks must pass and at least 2 approvals are required before merge. - **Require CI status checks to pass** — `enable_status_check: true` - **Required status check context** — `["status-check"]` (the consolidation job that verifies ALL CI jobs passed) -- **Require at least 2 review approvals** — `required_approvals: 2` (per - CONTRIBUTING.md "Review and Merge Requirements") +- **Require at least 1 review approval** — `required_approvals: 1` (Bot PRs + can merge with 1 approval; human PRs still need 2 per CONTRIBUTING.md) - **Dismiss stale reviews** — `dismiss_stale_approvals: true` - **Block on rejected reviews** — `block_on_rejected_reviews: true` - **Disallow direct pushes** — `enable_push: false` (all changes via PR) +**Note on approval requirements:** While branch protection is set to 1 approval +minimum, the CleverAgents bot workers are programmed to distinguish between +bot and human PRs. Bot PRs (containing "Automated by CleverAgents Bot" in the +description) will merge after 1 approval, while human PRs will wait for 2 +approvals as required by CONTRIBUTING.md. + **CRITICAL:** The `force_merge` API flag can bypass these rules. The agents are instructed to NEVER use it, but branch protection is the server-side enforcement. Configure it as strictly as possible. @@ -192,7 +198,7 @@ curl -s -X POST "https:///api/v1/repos///branch_protections" "enable_push": false, "enable_status_check": true, "status_check_contexts": ["status-check"], - "required_approvals": 2, + "required_approvals": 1, "dismiss_stale_approvals": true, "block_on_rejected_reviews": true, "block_on_outdated_branch": false, diff --git a/.opencode/agents/ca-quality-enforcer.md b/.opencode/agents/ca-quality-enforcer.md index d927f9fcc..86a54c559 100644 --- a/.opencode/agents/ca-quality-enforcer.md +++ b/.opencode/agents/ca-quality-enforcer.md @@ -61,7 +61,7 @@ curl -s -X POST "https:///api/v1/repos///branch_protections" "enable_push": false, "enable_status_check": true, "status_check_contexts": ["status-check"], - "required_approvals": 2, + "required_approvals": 1, "dismiss_stale_approvals": true, "block_on_rejected_reviews": true, "enable_merge_whitelist": false, diff --git a/.opencode/agents/issue-implementor.md b/.opencode/agents/issue-implementor.md index 9a8bc5bd0..f8754c677 100644 --- a/.opencode/agents/issue-implementor.md +++ b/.opencode/agents/issue-implementor.md @@ -205,6 +205,24 @@ function check_pr_work_needed(): "priority_score": pr_state.priority_score }) + # CRITICAL: Also check our own PRs from completed issues + for issue_num in completed_issues: + # Find PRs created by our workers for completed issues + our_prs = [pr for pr in all_open_prs if f"Closes #{issue_num}" in pr.body or f"Fixes #{issue_num}" in pr.body] + + for pr in our_prs: + if pr.number not in active_pr_workers and pr.number not in [p["pr"].number for p in prs_needing_work]: + # This is our PR but no worker is monitoring it! + pr_state = analyze_pr_state(pr) + if pr_state.needs_work: + # Add to high priority queue + prs_needing_work.insert(0, { + "pr": pr, + "work_type": pr_state.work_type, + "issue_number": issue_num, + "priority_score": 100 # Max priority for our own PRs + }) + # Check which PRs already have workers unassigned_prs = [] for pr_work in prs_needing_work: @@ -396,43 +414,60 @@ function dispatch_worker(mode, work_item, ref_summary): if mode == "pr-fix": # PR fix mode pr = work_item["pr"] - prompt = "You are an issue worker for Implementation operating in PR-FIX MODE. - mode: pr-fix - pr_number: - work_type: - issue_number: - branch: - pr_title: - Ref summary: - Forgejo PAT: . Git: . Username: . - - Your task: Fix this PR based on work_type: - - review-feedback: Implement requested changes from reviewers - - ci-fix: Fix failing CI checks - - merge-conflicts: Resolve conflicts with master - - ready-to-merge: Perform final checks and merge - - stale-check: Investigate why PR has stalled - - You own this PR until it is merged. Do not exit until merged or blocked by human feedback." + prompt = f"""You are an issue worker operating in PR-FIX MODE. + +CRITICAL: You are in mode: pr-fix + +pr_number: {pr.number} +work_type: {work_item["work_type"]} +issue_number: {work_item["issue_number"]} +branch: {pr.head.ref} +pr_title: {pr.title} + +Repository: {owner}/{repo} +Forgejo PAT: {forgejo_pat} +Git identity: {git_full_name} <{git_email}> +Forgejo username: {forgejo_username} + +Reference summary: {ref_summary} + +Your task: Fix this PR based on work_type: +- review-feedback: Implement requested changes from reviewers +- ci-fix: Fix failing CI checks +- merge-conflicts: Resolve conflicts with master +- ready-to-merge: Perform final checks and merge (YOU CAN MERGE AFTER 1 APPROVAL FOR BOT PRs) +- stale-check: Investigate why PR has stalled + +CRITICAL: You own this PR until it is merged. Monitor and handle all feedback. +Do not exit until the PR is merged or blocked by human feedback (needs feedback label).""" title = f"[CA-AUTO] worker-pr-fix: PR-{pr.number}" else: # issue-impl mode issue = work_item - prompt = "You are an issue worker for Implementation operating in ISSUE-IMPL MODE. - mode: issue-impl - Ref summary: - Issue: # - Branch: - Milestone: - Labels: - Forgejo PAT: . Git: . Username: . - Base branch: - - Your task: Implement this issue fully, create PR, and shepherd it through review until merged. - You own this issue from implementation through PR merge. Do not exit until the PR is merged." + prompt = f"""You are an issue worker operating in ISSUE-IMPL MODE. + +CRITICAL: You are in mode: issue-impl + +Issue: #{issue["number"]} — {issue["title"]} +Branch: {issue["branch"]} +Milestone: {issue["milestone"]} +Labels: {issue["labels"]} +Base branch: {issue.get("base_branch", "master")} + +Repository: {owner}/{repo} +Forgejo PAT: {forgejo_pat} +Git identity: {git_full_name} <{git_email}> +Forgejo username: {forgejo_username} + +Reference summary: {ref_summary} + +Your task: Implement this issue fully, create PR, and shepherd it through review until merged. + +CRITICAL: You own this issue from implementation through PR merge. +Do not exit until the PR is merged. Monitor and handle all review feedback.""" - title = f"[CA-AUTO] worker-issue-impl: issue-{issue.number}" + title = f"[CA-AUTO] worker-issue-impl: issue-{issue['number']}" # Create session SESSION_ID = bash("curl -s -X POST ${SERVER}/session \ @@ -484,6 +519,21 @@ LOOP FOREVER: # Actually dispatch PR fix worker using the helper function session_id = dispatch_worker("pr-fix", pr_work, ref_summary) + + # Verify the worker actually started + bash("sleep 2", timeout=5000) # Brief wait + verify_status = bash(f"curl -s ${SERVER}/session/{session_id}", timeout=30000) + + if "error" in verify_status.lower() or "not found" in verify_status.lower(): + print(f"[ERROR] Worker dispatch failed for PR #{pr_work['pr'].number}") + # Retry dispatch + session_id = dispatch_worker("pr-fix", pr_work, ref_summary) + bash("sleep 2", timeout=5000) + verify_status = bash(f"curl -s ${SERVER}/session/{session_id}", timeout=30000) + if "error" in verify_status.lower() or "not found" in verify_status.lower(): + print(f"[ERROR] Retry failed - skipping PR #{pr_work['pr'].number}") + continue + active_pr_workers[pr_work["pr"].number] = { "session_id": session_id, "work_type": pr_work["work_type"], @@ -493,10 +543,19 @@ LOOP FOREVER: slots_available -= 1 # Log dispatch - print(f"[{now()}] Dispatched PR-fix worker for PR #{pr_work['pr'].number} ({pr_work['work_type']})") + print(f"[{now()}] Dispatched PR-fix worker for PR #{pr_work['pr'].number} ({pr_work['work_type']}) - verified active") # ── STEP 3: Only dispatch to issues if ALL PRs have workers ────── - if not pr_work_queue and slots_available > 0: + # CRITICAL: Block ALL issue work if ANY PR needs attention + ALLOW_ISSUE_WORK = len(pr_work_queue) == 0 + + if not ALLOW_ISSUE_WORK: + # Clear issue queue to prevent accidental dispatch + if queue: + print(f"[PR-PRIORITY] {len(pr_work_queue)} PRs need work - clearing issue queue of {len(queue)} items") + queue = [] + + if ALLOW_ISSUE_WORK and slots_available > 0: # Fetch issues if queue is empty if not queue: # Only fetch issues when we actually have slots for them @@ -568,8 +627,24 @@ LOOP FOREVER: # Actually dispatch issue implementation worker session_id = dispatch_worker("issue-impl", issue_work, ref_summary) + + # Verify the worker actually started + bash("sleep 2", timeout=5000) # Brief wait + verify_status = bash(f"curl -s ${SERVER}/session/{session_id}", timeout=30000) + + if "error" in verify_status.lower() or "not found" in verify_status.lower(): + print(f"[ERROR] Worker dispatch failed for Issue #{issue.number}") + # Retry dispatch + session_id = dispatch_worker("issue-impl", issue_work, ref_summary) + bash("sleep 2", timeout=5000) + verify_status = bash(f"curl -s ${SERVER}/session/{session_id}", timeout=30000) + if "error" in verify_status.lower() or "not found" in verify_status.lower(): + print(f"[ERROR] Retry failed - skipping Issue #{issue.number}") + continue + active_issue_workers[issue.number] = session_id slots_available -= 1 + print(f"[{now()}] Dispatched issue worker for Issue #{issue.number} - verified active") # ── Monitor active workers: poll every 10 seconds ──────────── bash("sleep 10", timeout=30000) @@ -678,15 +753,30 @@ LOOP FOREVER: # ── Health signal every 10 cycles ───────────────────────────── if cycle % 10 == 0: + # Helper functions to format worker details + def format_pr_workers(active_pr_workers): + lines = [] + for pr_num, info in active_pr_workers.items(): + lines.append(f" - PR #{pr_num}: session {info['session_id'][:8]}... | type: {info['work_type']} | started: {info['assigned_at']}") + return "\n".join(lines) if lines else " (none)" + + def format_issue_workers(active_issue_workers): + lines = [] + for issue_num, session_id in active_issue_workers.items(): + lines.append(f" - Issue #{issue_num}: session {session_id[:8]}...") + return "\n".join(lines) if lines else " (none)" + forgejo_create_issue_comment( owner, repo, SESSION_STATE_ISSUE_NUMBER, body=f"[HEALTH] issue-implementor | Iteration: {cycle} | Status: active\n" + f"- Type: pool-supervisor\n" + f"- Max workers: {max_workers}\n" + f"- Total active workers: {len(active_pr_workers) + len(active_issue_workers)} / {max_workers}\n" + - f" - PR fix workers: {len(active_pr_workers)}\n" + - f" - Issue implementation workers: {len(active_issue_workers)}\n" + - f"- Work completed:\n" + + f"\nPR Fix Workers ({len(active_pr_workers)}):\n" + + format_pr_workers(active_pr_workers) + + f"\n\nIssue Implementation Workers ({len(active_issue_workers)}):\n" + + format_issue_workers(active_issue_workers) + + f"\n\n- Work completed:\n" + f" - PRs merged: {sum(1 for c in completed if c['type'] == 'pr')}\n" + f" - Issues completed: {len(completed_issues)}\n" + f"- Queues:\n" + @@ -703,6 +793,44 @@ LOOP FOREVER: # ── Idle check: If no active workers and no work, wait longer ──── total_active = len(active_pr_workers) + len(active_issue_workers) + + # More aggressive work discovery when we have capacity + if total_active < max_workers: + remaining_capacity = max_workers - total_active + print(f"[CAPACITY] {remaining_capacity} worker slots available - checking for work") + + # Always check PRs first + pr_work_queue = check_pr_work_needed() + + # Then check issues if still have capacity and no PRs need work + if not pr_work_queue and remaining_capacity > 0: + # Re-query issues even if queue is empty + try: + new_issues = forgejo_list_repo_issues( + owner, repo, + state="open", + labels="State/Verified,State/In Progress" + ) + # Filter to assigned issues not already being worked on + new_issues = [i for i in new_issues + if FORGEJO_USERNAME in [a.login for a in i.assignees] + and i.number not in active_issue_workers + and i.number not in completed_issues] + if new_issues: + queue.extend(new_issues) + # Re-sort by priority + queue.sort(key=lambda i: ( + 0 if "Type/Bug" in [l.name for l in i.labels] else 1, + i.milestone.number if i.milestone else 999, + 0 if "State/In Progress" in [l.name for l in i.labels] else 1, + {"Critical": 0, "High": 1, "Medium": 2, "Low": 3}.get( + next((l.name.split("/")[1] for l in i.labels if l.name.startswith("Priority/")), "Medium"), 2 + ) + )) + print(f"[CAPACITY] Found {len(new_issues)} new issues to work on") + except Exception as e: + print(f"[ERROR] Failed to fetch new issues: {e}") + if total_active == 0 and not pr_work_queue and not queue: # No active workers and no pending work - idle state print(f"[IDLE] No active workers or pending work. Waiting 60 seconds...")