fix(agents): Fix worker management, PR priority, and bot approval requirements
CI / unit_tests (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / security (push) Waiting to run
CI / quality (push) Waiting to run
CI / integration_tests (push) Waiting to run
CI / e2e_tests (push) Waiting to run
CI / coverage (push) Blocked by required conditions
CI / benchmark-regression (push) Blocked by required conditions
CI / benchmark-publish (push) Waiting to run
CI / build (push) Waiting to run
CI / docker (push) Blocked by required conditions
CI / helm (push) Waiting to run
CI / status-check (push) Blocked by required conditions

Fixed three critical issues in the CleverAgents autonomous system:

1. Worker Management: Enhanced issue-implementor health signaling to report
   detailed worker listings with session IDs and status. Added worker
   verification after dispatch to ensure workers actually start. Improved
   idle detection with aggressive work discovery when capacity is available.

2. PR Priority: Fixed PR work detection to include orphaned PRs from
   completed issues. Added absolute PR priority enforcement that blocks
   all issue work when any PR needs attention. Fixed worker dispatch
   prompts to clearly indicate operation mode (pr-fix vs issue-impl).

3. Bot Approval Requirements: Implemented single approval merging for bot
   PRs. Bot PRs (containing 'Automated by CleverAgents Bot') now merge
   with 1 approval while human PRs still require 2 per CONTRIBUTING.md.
   Updated branch protection to required_approvals: 1 with logic in agents
   to enforce the distinction. Added detection for approved-but-stuck PRs.

These changes ensure the system operates at maximum efficiency with proper
parallelism while maintaining quality gates through CI and code review.
This commit is contained in:
2026-04-05 18:18:15 +00:00
parent 97eebeb62c
commit 1bd0c7999d
5 changed files with 271 additions and 47 deletions
+42 -5
View File
@@ -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 \
+55 -2
View File
@@ -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)
+9 -3
View File
@@ -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://<HOST>/api/v1/repos/<owner>/<repo>/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,
+1 -1
View File
@@ -61,7 +61,7 @@ curl -s -X POST "https://<HOST>/api/v1/repos/<owner>/<repo>/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,
+164 -36
View File
@@ -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: <pr.number>
work_type: <work_item.work_type>
issue_number: <work_item.issue_number>
branch: <pr.head.ref>
pr_title: <pr.title>
Ref summary: <ref_summary (compact)>
Forgejo PAT: <PAT>. Git: <name> <email>. Username: <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: <ref_summary (compact)>
Issue: #<issue.number> — <issue.title>
Branch: <issue.branch>
Milestone: <issue.milestone>
Labels: <issue.labels>
Forgejo PAT: <PAT>. Git: <name> <email>. Username: <username>.
Base branch: <work_item.base_branch or 'master'>
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...")