Repository navigation
Conversation
…epair on failure, promote on merge Adds run_review_sweep, run every 60s by a new spawn_supervisor_review_sweep daemon task, mirroring the existing spawn_supervisor_discovery pattern. It lists items in the in_review state group and polls PR/CI status for each via a new worktree::pr_ci_status (reusing github::mcp::checks_wait_summary's existing check-run classification): - Merged -> calls item_check_merge directly (now pub(crate), same as item_claim) to promote to completed and clean up the worktree. - Failing -> dispatches a self-repair job via the same in-process agentflare-work job type run_discovery_tick already uses. Re-running agentflare work on an already-in_review item reclaims its existing worktree/branch and pushes a fix commit rather than starting over. A marker comment both informs the agent what's failing (via build_prompt's prior-discussion inclusion) and doubles as the attempt counter enforcing quota::decide::SELF_REPAIR_CAP before gating the item for a human instead of retrying forever. - Pending/Passing/Unknown -> left alone for the next tick. A job_in_flight check against the job queue closes the race window between enqueueing and the job actually reaching item_claim; the in_review state group itself (which a claimed job's item_claim moves the item out of) is the primary guard against double-dispatch. Agentflare-Agent: claude-code Agentflare-Branch: task/65 Agentflare-Item: 65
📝 WalkthroughWalkthroughThe supervisor now polls in-review items every 60 seconds, classifies pull-request CI status, promotes merged items, and dispatches bounded self-repair jobs. The dashboard starts the sweep during server initialization and includes smoke and behavior tests. ChangesSupervisor review automation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant DashboardServer
participant SupervisorSweep
participant GitHubChecks
participant WorkQueue
DashboardServer->>SupervisorSweep: start periodic review sweep
SupervisorSweep->>GitHubChecks: classify pull-request status
GitHubChecks-->>SupervisorSweep: return CI or merge status
SupervisorSweep->>WorkQueue: enqueue eligible self-repair job
SupervisorSweep-->>DashboardServer: report sweep result
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/worktree.rs (1)
104-132: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider deriving
is_pr_mergedfrompr_ci_status.
pr_ci_statusrepeats the whole PR-resolution preamble ofis_pr_merged(Lines 68-88): branch name,RepoId::resolve_from_remote,Client::new,pulls::find_existing, and the merged check. The two functions must stay in sync for the sweep and foritem_check_mergeto agree on "merged".
is_pr_mergedcan delegate:♻️ Proposed consolidation
pub fn is_pr_merged(item: &agentflare_backend::item::Item, repo_root: &Path) -> bool { - let branch = format!("task/{}", item.sequence_id); - let Some(repo) = RepoId::resolve_from_remote(repo_root) else { - return false; - }; - let client = match crate::github::Client::new() { - Ok(c) => c, - Err(_) => return false, - }; - match crate::github::pulls::find_existing(&client, &repo, &branch) { - Ok(Some(pr)) => pr.merged_at.is_some(), - Ok(None) => false, - Err(e) => { - eprintln!( - "worktree: could not check merge status for item {}: {e}", - item.id - ); - false - } - } + matches!(pr_ci_status(item, repo_root), PrCiStatus::Merged) }This costs one extra check-runs call only when the PR is not merged, because
pr_ci_statusreturnsMergedbefore it fetches checks.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/worktree.rs` around lines 104 - 132, Refactor is_pr_merged to delegate to pr_ci_status and return true only when the result is PrCiStatus::Merged, removing its duplicated branch/repository/client/PR resolution and merged-check logic. Preserve pr_ci_status’s existing behavior and avoid additional checks beyond its early merged return.src/supervisor.rs (1)
278-346: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSweep structure and fixtures look sound; the promote path has no test.
run_review_sweepreads items, states, and labels in onewith_backend_dbclosure, then runs the blocking GitHub calls outside the lock. That matches the pattern used byrun_discovery_tickand byitem_claim.The new fixtures cover unknown status, dispatch, cap, already-gated, and in-flight.
promote_merged_item(Lines 348-360) has no test. It parsesitem_check_merge's JSON and returnspromoted. Ifitem_check_merge's response key ever changes, this silently counts every merged item asskipped. A test that seeds anin_reviewitem and asserts thepromoted == false/reasonshape fromitem_check_mergewould pin the contract without network access.Also applies to: 348-360, 914-1195
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/supervisor.rs` around lines 278 - 346, Add a focused test for promote_merged_item that seeds an in_review item and exercises the item_check_merge response without network access. Assert the parsed promoted boolean and reason fields match the expected response shape, including the false case, so response-key changes cannot silently turn merged items into skipped results. Reuse the existing sweep fixtures and test helpers.src/dashboard/server.rs (1)
221-231: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winSet
MissedTickBehavior::Delayon the sweep ticker.If a sweep exceeds
interval, the defaultBurstbehavior can run several sweeps back to back.Delaylimits catch-up to one tick and schedules the following tick one interval later. It does not wait before the first post-overrun sweep; use a completion-based sleep if every sweep requires a full interval between starts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/dashboard/server.rs` around lines 221 - 231, Configure the ticker in the spawned sweep loop to use tokio’s MissedTickBehavior::Delay immediately after creating it. Keep the existing interval and sweep execution flow unchanged; only alter missed-tick handling so overruns do not trigger back-to-back sweeps.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/supervisor.rs`:
- Around line 390-435: Update the cap handling in the supervisor sweep around
already_gated and the prior_attempts cap branch so an existing “CI self-repair
cap reached” comment also prevents repeated comments when the gate label is
absent. Ensure the cap path creates or otherwise applies the
NEEDS_HUMAN_GATE_LABEL before returning, rather than silently skipping it when
label_id_by_name lacks the entry. Add coverage for a cap case without that label
and verify a second sweep creates no additional comment.
- Around line 367-376: Update job_in_flight to use a queue-side active-job
lookup that searches queued and running jobs without the Queue::list 100-row
limit, adding or reusing a Queue method such as find_active_by_arg with SQL
filtering. Match only agentflare-work jobs whose first argument args[0] equals
item_id, rather than searching every argument position, and preserve the boolean
result used to prevent duplicate repair jobs.
In `@src/worktree.rs`:
- Around line 130-147: Update pr_ci_status and the checks_wait_summary flow so
completed check runs with a null conclusion remain Pending rather than Passing,
and incorporate commit statuses alongside check runs when classifying CI. Extend
list_check_runs or the surrounding GitHub actions logic to fetch statuses for
the same SHA, then ensure failing status-only gates produce Failed while
preserving Pending for absent or unresolved checks.
---
Nitpick comments:
In `@src/dashboard/server.rs`:
- Around line 221-231: Configure the ticker in the spawned sweep loop to use
tokio’s MissedTickBehavior::Delay immediately after creating it. Keep the
existing interval and sweep execution flow unchanged; only alter missed-tick
handling so overruns do not trigger back-to-back sweeps.
In `@src/supervisor.rs`:
- Around line 278-346: Add a focused test for promote_merged_item that seeds an
in_review item and exercises the item_check_merge response without network
access. Assert the parsed promoted boolean and reason fields match the expected
response shape, including the false case, so response-key changes cannot
silently turn merged items into skipped results. Reuse the existing sweep
fixtures and test helpers.
In `@src/worktree.rs`:
- Around line 104-132: Refactor is_pr_merged to delegate to pr_ci_status and
return true only when the result is PrCiStatus::Merged, removing its duplicated
branch/repository/client/PR resolution and merged-check logic. Preserve
pr_ci_status’s existing behavior and avoid additional checks beyond its early
merged return.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: c5b97940-671a-4f10-a8cc-1864ac5d152c
📒 Files selected for processing (4)
src/dashboard/server.rssrc/mcp_server/item.rssrc/supervisor.rssrc/worktree.rs
| fn job_in_flight(queue: &agentflare_jobs::Queue, item_id: &str) -> bool { | ||
| [ | ||
| agentflare_jobs::JobState::Queued, | ||
| agentflare_jobs::JobState::Running, | ||
| ] | ||
| .into_iter() | ||
| .filter_map(|state| queue.list(Some(state)).ok()) | ||
| .flatten() | ||
| .any(|job| job.args.contains(&item_id.to_string())) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
job_in_flight can miss an in-flight job, because Queue::list caps at 100 rows.
Queue::list runs ORDER BY created_at DESC LIMIT 100 (crates/agentflare-jobs/src/queue.rs Lines 241-265). job_in_flight therefore inspects only the 100 newest queued jobs and the 100 newest running jobs. If more than 100 jobs are queued, the guard returns false for an item that already has a job, and the sweep enqueues a duplicate repair job every tick. The claim lease bounds the damage, because the second job's item_claim returns Held, but the queue and the marker-comment count still grow on each tick.
A second, smaller point: job.args.contains(...) matches the item id in any argument position. Only args[0] is the item id for agentflare-work jobs. Match the command and the first argument instead.
🔧 Proposed narrowing
fn job_in_flight(queue: &agentflare_jobs::Queue, item_id: &str) -> bool {
[
agentflare_jobs::JobState::Queued,
agentflare_jobs::JobState::Running,
]
.into_iter()
.filter_map(|state| queue.list(Some(state)).ok())
.flatten()
- .any(|job| job.args.contains(&item_id.to_string()))
+ .any(|job| job.command == "agentflare-work" && job.args.first().is_some_and(|a| a == item_id))
}The LIMIT 100 blind spot needs a queue-side lookup, for example a Queue::find_active_by_arg method that filters in SQL rather than in Rust.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/supervisor.rs` around lines 367 - 376, Update job_in_flight to use a
queue-side active-job lookup that searches queued and running jobs without the
Queue::list 100-row limit, adding or reusing a Queue method such as
find_active_by_arg with SQL filtering. Match only agentflare-work jobs whose
first argument args[0] equals item_id, rather than searching every argument
position, and preserve the boolean result used to prevent duplicate repair jobs.
| let already_gated = label_id_by_name | ||
| .get(NEEDS_HUMAN_GATE_LABEL) | ||
| .is_some_and(|gate_id| { | ||
| mcp.with_backend_db(|conn| agentflare_backend::item::list_labels(conn, &item.id)) | ||
| .ok() | ||
| .and_then(Result::ok) | ||
| .is_some_and(|ids| ids.contains(gate_id)) | ||
| }); | ||
| if already_gated || job_in_flight(queue, &item.id) { | ||
| return false; | ||
| } | ||
|
|
||
| let prior_attempts = mcp | ||
| .with_backend_db(|conn| agentflare_backend::comment::list_by_item(conn, &item.id)) | ||
| .ok() | ||
| .and_then(Result::ok) | ||
| .map(|comments| { | ||
| comments | ||
| .iter() | ||
| .filter(|c| c.body.starts_with(CI_SELF_REPAIR_MARKER)) | ||
| .count() as u32 | ||
| }) | ||
| .unwrap_or(0); | ||
|
|
||
| if prior_attempts >= crate::quota::decide::SELF_REPAIR_CAP { | ||
| let _ = mcp.comment_impl(CommentRequest { | ||
| action: "create".into(), | ||
| item_id: Some(item.id.clone()), | ||
| body: Some(format!( | ||
| "## supervisor — CI self-repair cap reached\n\nFailing checks: {}. \ | ||
| {} automatic repair attempt(s) already made with no green build — needs a human look.", | ||
| failed_checks.join(", "), | ||
| crate::quota::decide::SELF_REPAIR_CAP, | ||
| )), | ||
| ..Default::default() | ||
| }); | ||
| if let Some(gate_id) = label_id_by_name.get(NEEDS_HUMAN_GATE_LABEL) { | ||
| let _ = mcp.item_add_label(ItemRequest { | ||
| action: "add_label".into(), | ||
| id: Some(item.id.clone()), | ||
| label_id: Some(gate_id.clone()), | ||
| ..Default::default() | ||
| }); | ||
| } | ||
| return false; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
The cap branch posts a new comment on every tick when the needs-human-gate label does not exist.
already_gated at Lines 390-397 can only become true if label_id_by_name contains NEEDS_HUMAN_GATE_LABEL. Line 426 also adds the label only if that entry exists. If the project has no needs-human-gate label, then:
- The cap branch posts a "cap reached" comment.
- No label is applied, so
already_gatedstaysfalse. prior_attemptsstays at the cap, because the cap comment does not useCI_SELF_REPAIR_MARKER.- The next sweep tick repeats step 1.
The result is one new comment per item every 60 seconds, forever, with no human-visible gate. run_review_sweep builds label_id_by_name from the project's existing labels only, so a project that never created this label hits this path.
Gate on the comment as well as the label, and create the label when it is missing.
🐛 Proposed guard
+const CI_SELF_REPAIR_CAP_MARKER: &str = "## supervisor — CI self-repair cap reached";
+
fn self_repair_or_gate(- let prior_attempts = mcp
+ let comments = mcp
.with_backend_db(|conn| agentflare_backend::comment::list_by_item(conn, &item.id))
.ok()
.and_then(Result::ok)
- .map(|comments| {
- comments
- .iter()
- .filter(|c| c.body.starts_with(CI_SELF_REPAIR_MARKER))
- .count() as u32
- })
- .unwrap_or(0);
+ .unwrap_or_default();
+ let prior_attempts = comments
+ .iter()
+ .filter(|c| c.body.starts_with(CI_SELF_REPAIR_MARKER))
+ .count() as u32;
+ let already_capped = comments
+ .iter()
+ .any(|c| c.body.starts_with(CI_SELF_REPAIR_CAP_MARKER));
if prior_attempts >= crate::quota::decide::SELF_REPAIR_CAP {
+ if already_capped {
+ return false;
+ }
let _ = mcp.comment_impl(CommentRequest {
action: "create".into(),
item_id: Some(item.id.clone()),
body: Some(format!(
- "## supervisor — CI self-repair cap reached\n\nFailing checks: {}. \
+ "{CI_SELF_REPAIR_CAP_MARKER}\n\nFailing checks: {}. \
{} automatic repair attempt(s) already made with no green build — needs a human look.",Add a test that covers the cap path with no needs-human-gate label in label_id_by_name, and assert that a second call adds no second comment.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/supervisor.rs` around lines 390 - 435, Update the cap handling in the
supervisor sweep around already_gated and the prior_attempts cap branch so an
existing “CI self-repair cap reached” comment also prevents repeated comments
when the gate label is absent. Ensure the cap path creates or otherwise applies
the NEEDS_HUMAN_GATE_LABEL before returning, rather than silently skipping it
when label_id_by_name lacks the entry. Add coverage for a cap case without that
label and verify a second sweep creates no additional comment.
| let Some(sha) = pr.head.as_ref().map(|h| h.sha.clone()) else { | ||
| return PrCiStatus::Unknown; | ||
| }; | ||
| let checks = match crate::github::actions::list_check_runs(&client, &repo, &sha) { | ||
| Ok(c) => c, | ||
| Err(e) => { | ||
| eprintln!( | ||
| "worktree: could not fetch check status for item {}: {e}", | ||
| item.id | ||
| ); | ||
| return PrCiStatus::Unknown; | ||
| } | ||
| }; | ||
| let summary = crate::github::mcp::checks_wait_summary(&checks, 0); | ||
| let total = summary["total_checks"].as_u64().unwrap_or(0); | ||
| if total == 0 || summary["pending"].as_bool().unwrap_or(true) { | ||
| return PrCiStatus::Pending; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Confirm the PR head type and check-run classification inputs.
set -euo pipefail
fd -t f 'pulls.rs' src | xargs -r rg -n -C4 'pub struct|sha|git_ref'
fd -t f 'actions.rs' src | xargs -r rg -n -C4 'pub struct CheckRun|status|conclusion'
rg -n -C6 'wait_for_checks' srcRepository: getappz/agentflare
Length of output: 6472
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- candidate files ---'
fd -t f . src | rg '/(pulls|actions|mcp|worktree|models)\.(rs|toml)$' || true
printf '%s\n' '--- PullRequest and head definitions ---'
rg -n -C8 'struct PullRequest|struct.*Head|pub head|git_ref|sha' src
printf '%s\n' '--- check summary implementation ---'
rg -n -C12 'checks_wait_summary|total_checks|pending|conclusion|check_runs' src
printf '%s\n' '--- status/check API callers ---'
rg -n -C6 'list_check_runs|list_status|commit.*status|statuses|pr_ci_status' srcRepository: getappz/agentflare
Length of output: 50375
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- relevant files ---'
fd -t f . src/github | rg '(models|pulls|actions|mcp)\.rs$'
printf '%s\n' '--- pull request model ---'
rg -l 'struct PullRequest|struct PullRequestHead|pub struct PullRequest' src/github | while read -r f; do
echo "FILE: $f"
rg -n -C12 'struct PullRequest|struct PullRequestHead|pub head|git_ref|sha' "$f"
done
printf '%s\n' '--- action endpoints and check-run helper ---'
for f in $(fd -t f . src/github | rg '(actions|mcp)\.rs$'); do
echo "FILE: $f"
rg -n -C10 'list_check_runs|check_runs|checks_wait_summary|total_checks|pending|conclusion|status' "$f" || true
done
printf '%s\n' '--- worktree and callers ---'
sed -n '90,175p' src/worktree.rs
sed -n '1085,1165p' src/cli/git.rsRepository: getappz/agentflare
Length of output: 25680
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
# Read-only model of the classification described in the review comment.
def classify(run):
conclusion = run.get("conclusion")
status = run.get("status")
if status != "completed":
return "pending"
if conclusion not in {"success", "neutral", "skipped"}:
return "failed"
return "passing"
cases = [
{"status": "completed", "conclusion": None},
{"status": "completed", "conclusion": "failure"},
{"status": "in_progress", "conclusion": None},
{"status": "completed", "conclusion": "success"},
]
for case in cases:
print(case, "=>", classify(case))
PYRepository: getappz/agentflare
Length of output: 386
🌐 Web query:
GitHub REST API commits SHA check-runs commit statuses difference /commits/{ref}/check-runs /commits/{ref}/statuses
💡 Result:
The primary difference between the GitHub REST API commit statuses (/commits/{ref}/statuses) and check-runs (/commits/{ref}/check-runs) lies in their intended use cases, capabilities, and accessibility [1][2]. Commit Statuses Commit statuses are a simpler, legacy mechanism primarily designed for external services and integrations to report a binary state (success, failure, error, or pending) for a specific commit [3][1][2]. - Capability: Supports a state, description, target URL, and context string to differentiate multiple statuses [3]. - Access: Available to OAuth apps and authenticated users with appropriate repository permissions [4][5]. - Identification: Distinguished by a user-provided context string [3][6]. Check-runs Check-runs are part of the more modern Checks API, which provides a rich, detailed interface for CI/CD and other automated tools [6][1][5]. - Capability: Supports detailed output, line-specific code annotations, summary text, and multiple states/conclusions (e.g., neutral, cancelled, timed_out, action_required) [1][5][2]. They belong to a "check-suite" and are managed through the Checks API rather than the Commits API [6][5][7]. - Access: Write access is restricted exclusively to GitHub Apps [4][5][7]. OAuth apps and users can only view check-runs [4][5]. - Identification: Distinguished by an auto-generated check_run_id [6]. In summary, commit statuses are best for basic integrations where you simply need to signal a pass/fail state, whereas check-runs are the preferred standard for GitHub Apps providing integrated, detailed feedback within the GitHub UI [6][1][5]. If you are not building a GitHub App, you are generally limited to using commit statuses to create new status information [4][5].
Citations:
- 1: https://docs.github.com/en/pull-requests/reference/status-checks
- 2: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks
- 3: https://docs.github.com/en/rest/commits/statuses
- 4: https://docs.github.com/en/rest/checks/runs
- 5: https://docs.github.com/en/rest/guides/using-the-rest-api-to-interact-with-checks
- 6: https://stackoverflow.com/questions/67919168/github-checks-api-vs-check-runs-vs-check-suites
- 7: https://docs.github.com/rest/guides/getting-started-with-the-checks-api
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
def rust_failed(conclusion):
return conclusion is not None and conclusion not in {"success", "neutral", "skipped"}
def rust_summary(runs):
pending = [r["name"] for r in runs if r["status"] != "completed"]
failed = [r["name"] for r in runs if rust_failed(r.get("conclusion"))]
return {"pending": bool(pending), "failed_checks": failed, "total_checks": len(runs)}
runs = [
{"name": "completed-null", "status": "completed", "conclusion": None},
{"name": "in-progress", "status": "in_progress", "conclusion": None},
{"name": "failure", "status": "completed", "conclusion": "failure"},
]
summary = rust_summary(runs)
print(summary)
print("completed-null-only =>", rust_summary([runs[0]]))
PY
printf '%s\n' '--- PrCiStatus consumers ---'
rg -n -C10 'PrCiStatus::(Failing|Passing|Pending|Unknown|Merged)|pr_ci_status' srcRepository: getappz/agentflare
Length of output: 10030
Treat completed checks with no conclusion as pending.
PullRequest.head uses RefInfo, which exposes sha; no change is needed for that field. However, checks_wait_summary excludes a completed run with conclusion == null from both pending and failed_checks. pr_ci_status then returns Passing, so supervisor::run_review_sweep skips self-repair.
list_check_runs queries only /commits/{sha}/check-runs. It does not query /commits/{sha}/statuses. A failing status-only required gate is therefore invisible. If another check run exists, the PR can be classified as Passing; if no check runs exist, it remains Pending. Handle missing conclusions as pending and include commit statuses in the CI classification.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/worktree.rs` around lines 130 - 147, Update pr_ci_status and the
checks_wait_summary flow so completed check runs with a null conclusion remain
Pending rather than Passing, and incorporate commit statuses alongside check
runs when classifying CI. Extend list_check_runs or the surrounding GitHub
actions logic to fetch statuses for the same SHA, then ensure failing
status-only gates produce Failed while preserving Pending for absent or
unresolved checks.
Reconciles #63's per-project discovery-tick rewrite with #435's autonomous in-review sweep (run_review_sweep/self_repair_or_gate), both of which touched supervisor.rs independently after this branch was cut. - enqueue_work_job (new in #435, shared by dispatch_item and self_repair_or_gate) now takes an optional folder_path so #63's per-project dispatch can still thread its item's own project directory through, while self_repair_or_gate (still single-project via run_review_sweep's cwd-based resolve_project) passes None. - Test additions from both branches kept intact and separated (seed_ready_item_in_project/its regression test from #63; throwaway_repo/seed_in_review_item/seed_gate_label and the six review-sweep tests from #435) rather than interleaved -- git's merge algorithm had conflated them due to structural similarity between two unrelated new functions. All 19 supervisor:: tests pass, fmt clean, clippy clean (CI flags). Agentflare-Agent: claude-code Agentflare-Branch: task/63 Agentflare-Item: 63
… CPU pressure (#459) * wip: agentflare-resource-gate crate + supervisor/dashboard integration (uncommitted work rescued from task/435 worktree) Agentflare-Agent: claude-code_2-1-228_harness Agentflare-Branch: wip/resource-gate * ## Host resource gate for autonomous dispatch (item #435) Adds `agentflare-resource-gate`, a new crate providing a process-wide, continuously-sampled host-pressure tier that gates autonomous dispatch independently of the existing per-agent `auth_db` cooldown check. **What it does:** - `signals.rs` — samples global CPU usage via a *fresh* `sysinfo::System` per call (regression guard against a Sentry-reported panic when a long-lived `System` outlives a runtime core-count change), plus a lightweight server/container/headless heuristic (`AGENTFLARE_DEPLOYMENT` env override, k8s, `/.dockerenv`, no-display-on-Linux). - `policy.rs` — pure `decide(signals, cfg) -> Policy` with tiers `Aggressive` (server/always-on) → `Normal` → `Throttled` → `Paused`, in a documented evaluation order (user override → server mode → CPU severity → CPU busy threshold), thresholds clamped so a malformed config can't silently disable or force-throttle dispatch. - `gate.rs` — a `OnceLock`-backed singleton: `init_global()` starts a 30s background sampler (idempotent, safe to call more than once), and `current_policy()` is a cheap cached read defaulting to `Normal` if never initialized. - `pool_size.rs` — the pre-existing `dashboard::concurrency` worker-pool sizing logic, relocated here as it's the same "how much can this host handle" concern (`work_max_concurrency`'s CPU+memory-aware sizing). **Integration:** `supervisor::run_discovery_tick` and `self_repair_or_gate` now take a `host_policy: Policy` parameter and skip dispatch (counting the item as `waiting`, not vanishing it) when `host_policy.blocks_dispatch()` — independent of, and alongside, the existing `is_cooling_down` agent check. `dashboard/server.rs::run` calls `init_global()` once at daemon startup and threads the live policy into both dispatch call sites. **Scope decisions:** - Battery/AC probing (present in the openhuman reference design) is deliberately deferred — agentflare's autonomous `work` runs mostly on always-on dev boxes/servers, and pulling in `starship-battery` adds a real macOS FFI dependency chain for a signal unlikely to matter. Documented in `signals.rs` as a `Policy`/`Signals` field addable later with no API break. - The per-worker concurrency semaphore described in the item's "proposed shape" (hard-cap concurrent dispatch under `Throttled`/`Paused`) is not implemented — out of scope for the "Done when" bar, which only requires `current_policy()` existing and consulted by both dispatch paths plus a CPU-pressure test proving it blocks dispatch. Left as a natural follow-up. - `src/supervisor.rs`'s 3 new `host_gate_tests` were split into `src/supervisor_host_gate_tests.rs` (via `#[path]`) purely to stay under the repo's 1500-line LOC gate — no logic change. - `crates/agentflare-jobs/src/supervisor.rs` had picked up an unrelated `task/440` dead-code fix borrowed for local clippy testing (its own diff said "will be reverted") — reverted back to master's version; that fix belongs to `task/440`, not this item. **Verification:** `cargo test -p agentflare --bin agentflare supervisor::` — 22/22 passed including the 3 new host-gate tests. `cargo test -p agentflare-resource-gate` — 23/23 passed. `cargo clippy -p agentflare -p agentflare-resource-gate --all-targets -- -A unsafe_code -A clippy::pedantic` — zero warnings. A full `--workspace -D warnings` clippy run is blocked by the pre-existing (master, Windows-only) `agentflare-jobs` dead-code/unused-import issue that `task/440` already fixes separately — intentionally left untouched rather than folding an unrelated fix into this PR. This branch also recovers uncommitted work-in-progress that was sitting in this item's worktree from an earlier, incomplete session (never committed, discovered during an unrelated branch/worktree cleanup pass) — the crate and integration above are that work, finished and verified. Agentflare-Agent: claude-code_2-1-228_harness Agentflare-Branch: task/435 Agentflare-Item: 435 * fix(supervisor): resolve host-gate test module on Linux, guard inverted CPU thresholds, defer instead of skip Three fixes on top of the resource-gate work: - The host-gate test module used a #[path] override with a '..' traversal (src/supervisor/tests/../../). Windows resolved it lexically, but Linux walks the real path and those intermediate directories don't exist, so fmt/clippy/build all failed in CI. Moved the file to the location Rust's default module resolution already looks for and dropped #[path] entirely. - policy::decide clamped cpu_busy_threshold_pct and cpu_severe_pct to 0..100 independently, which still admits an inverted pair (busy >= severe). That collapses the tier ladder -- every reading above severe pauses before it can be judged merely busy, making Throttled unreachable. GateConfig's fields are public, so the ordering guard has to live in decide() rather than only in from_env(). Falls back to the shared default pair. - self_repair_or_gate returned a bool, so a cooldown or host-pressure block was counted as 'skipped' -- which reads to an operator as a decision that won't be revisited, when in fact the next sweep retries it. Replaced with a SelfRepairOutcome enum and added ReviewSweepResult.waiting, mirroring DiscoveryTickResult.waiting from item #82. Agentflare-Agent: claude-code_2-1-228_agent Agentflare-Branch: task/435 Agentflare-Item: 435 * refactor(resource-gate): normalize thresholds at the env boundary too, not just in decide() CodeRabbit's config.rs finding asked for the ordering guard in GateConfig::from_env as well, which the previous commit only added to policy::decide. That was the weaker half: env vars are the actual user-facing input path, so from_env could hand back a config whose threshold pair disagreed with what the gate would really enforce -- misleading to anything else reading those fields (a log line, a future dashboard tile), even though decide() recovered internally. Extracted the clamp-and-order logic into GateConfig::normalized() so there is one implementation, applied at the env boundary and re-applied (idempotently) in decide() -- still needed there because the fields are public and a hand-built GateConfig never passes through from_env. Agentflare-Agent: claude-code_2-1-228_agent Agentflare-Branch: task/435 Agentflare-Item: 435
Closes #65.
No autonomous loop drove an item from "in_review" (PR open) to "completed"
(PR merged), or repaired a PR with failing CI —
item_check_mergeonlypromoted on-demand when something called it manually, and nothing polled it
periodically. Confirmed live: item #62's PR (#434) sat with broken CI for
its entire lifetime with zero autonomous response.
What changed
Adds
supervisor::run_review_sweep, run every 60s by a newspawn_supervisor_review_sweepdaemon task (src/dashboard/server.rs),mirroring the existing
spawn_supervisor_discovery/run_discovery_tickpattern.
It lists items in the
in_reviewstate group (no label needed — stategroup is itself the query, and doubles as the concurrency guard: a
self-repair job's
item_claimmoves the item tostartedfor itsduration, so an item with a repair already running never re-appears here).
For each item it polls PR/CI status via a new
worktree::pr_ci_status,which reuses the exact "total>0 && !pending" check-run classification
cli::git::wait_for_checksalready uses (github::mcp::checks_wait_summary),just applied once per tick instead of in a loop:
item_check_mergedirectly (bumped frompub(super)to
pub(crate), same asitem_claim) to promote tocompletedandclean up the worktree — no logic duplicated.
agentflare-workin-process job type
run_discovery_tickalready uses (factored thejob-enqueue call into a shared
enqueue_work_jobhelper). Re-runningagentflare workon an already-in_review item reclaims its existingworktree/branch and pushes a fix commit rather than starting over. A
marker comment (
## supervisor — CI self-repair dispatched) both informsthe agent what's failing (via
build_prompt's prior-discussion inclusion)and doubles as the attempt counter — once
quota::decide::SELF_REPAIR_CAP(3) marker comments exist with CI still red, it gates the item with the
needs-human-gatelabel instead of retrying forever.A
job_in_flightcheck against the job queue closes the small race windowbetween enqueueing and the job actually reaching
item_claim.Test plan
cargo test --bin agentflare— full suite green except onepre-existing, unrelated failure (
code::confirm::tests::fallback_scan_finds_matches_and_skips_target_dir,confirmed failing identically on unmodified master in this worktree)
cargo clippy --bin agentflare --tests— cleancargo fmt --all --check— cleansupervisor.rstests coveringjob_in_flight, cap-reachedgating, already-gated quiet path, in-flight double-dispatch guard,
and
run_review_sweep's state-group filter + unknown-PR-statussoft-fail
spawn_supervisor_review_sweepSummary by CodeRabbit
New Features
Bug Fixes
Tests