Skip to content

feat: add supervisor auto-dispatch loop (item #408) - #376

Merged
getappz merged 17 commits into
masterfrom
task/supervisor-auto-dispatch
Jul 30, 2026
Merged

feat: add supervisor auto-dispatch loop (item #408)#376
getappz merged 17 commits into
masterfrom
task/supervisor-auto-dispatch

Conversation

@getappz

@getappz getappz commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Implements supervisor auto-dispatch: periodically scans ready-for-work items and dispatches to confirmed agents (claude-code).

Changes

  • crates/agentflare-backend/src/item.rs — add list_by_label query
  • src/supervisor.rs — new module: resolve_confirmed_agent, run_discovery_tick, skip_item, dispatch_item with 5 tests
  • src/main.rs — register supervisor module
  • src/mcp_server/item.rs — bump item_add_label / item_remove_label to pub(crate)
  • src/mcp_server.rs — add for_test_memory() constructor & backend_db_overridepub(crate)
  • src/dashboard/server.rs — wire spawn_supervisor_discovery (12s interval) after job cleanup

Closes #408

Summary by CodeRabbit

  • New Features
    • Added a periodic background discovery process for items marked “ready-for-work”.
    • Automatically dispatches eligible items to confirmed autonomous agents, queues work, and updates labels/comments with job details.
    • Marks ineligible items for manual dispatch with an explanation.
    • Added label-based item querying to fetch non-deleted items carrying a specific label.
  • Bug Fixes
    • Prevents dispatching items to unconfirmed or unknown assignees.
  • Tests
    • Added coverage for label-based item listing and supervisor discovery behavior.

getappz added 16 commits July 28, 2026 15:24
item(list) had no cap when limit was omitted, unlike search/groom,
so a bare call on a large project could overflow the MCP response
token limit (155 items / 52k chars observed). Default it to 50
(capped at 500) and return a pagination envelope
(total/offset/limit/next_offset/prev_offset) so callers can page
forward/backward without re-deriving offsets by hand.
…inding

- cargo fmt wrap on the next_offset line (CI fmt check was failing)
- prev_offset now clamps via offset.min(total) so an out-of-range offset
  navigates back into range instead of repeating an empty page, and both
  next_offset/prev_offset suppress when limit == 0
enforced_rule_reason_for_tool matched on tool_name alone, so the
"usetsearch" coaching rule denied every ToolSearch call regardless of
what it was searching for -- including WebFetch/EnterPlanMode/
mcp__claude-in-chrome__* and agentflare's own mcp__flare__* tools,
contradicting the rule's own stated fallback. It now inspects the
query and only blocks genuine leanctx/ctx_* lookups, the one
namespace actually missing from the deferred-tool list.

Also add a builtin-tool catalog so mcp__flare__tool(action="search")
finds agentflare's own first-party tools (item, asset, handoff, ...)
before falling back to the public MCP registry -- previously a query
like "item" returned unrelated third-party servers because the
gateway registry only indexes configured downstream servers.
- Content filter now only applies to the Builtin-tier "usetsearch"
  rule; a user override (same id, tier Override) enforces on tool
  name alone again, per the coaching system's "override always wins"
  contract.
- Normalize spaces/hyphens to underscores before the leanctx/ctx_*
  keyword match, so "ctx read" or "lean ctx" still trigger it, not
  just the literal underscored form.
- Replace the unguaranteed -1.0 score sentinel on builtin tool hits
  with a documented 0.0 constant that's provably worse than any real
  bm25 score and better than the registry fallback sentinel.
- Strengthen merge_builtin_hits test coverage to assert hit source
  per index, not just result length.
- search_builtin_tools now accepts and honors MatchMode: All requires
  every query term to match, Any requires at least one. Previously it
  always did Any-style matching regardless of the caller's requested
  mode, so mode="all" (the tool's own default) could still surface a
  builtin hit like mcp__flare__item on a query such as "item
  nonexistent" that shouldn't have matched anything.
- Updated the "tool" action's description (both the builtin catalog
  entry and the matching #[tool] attribute) to state that
  action="search" now returns first-party agentflare tools alongside
  downstream MCP server tools, not just downstream ones.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 29f07abb-3c29-4de5-acdc-85683d4188cb

📥 Commits

Reviewing files that changed from the base of the PR and between b013d3f and c40bc42.

📒 Files selected for processing (1)
  • src/supervisor.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/supervisor.rs

📝 Walkthrough

Walkthrough

Adds label-scoped item retrieval and a supervisor that periodically scans ready-for-work items, dispatches them to confirmed autonomous agents, or marks them for manual dispatch. The dashboard starts the discovery loop, with in-memory tests covering the new behavior.

Changes

Supervisor discovery and dispatch

Layer / File(s) Summary
Label-scoped item access and mutation visibility
crates/agentflare-backend/src/item.rs, src/mcp_server/item.rs, src/mcp_server.rs
Adds list_by_label, validates label filtering, widens label mutation visibility, and provides an in-memory MCP test constructor.
Supervisor discovery and item transitions
src/supervisor.rs
Scans ready-for-work items, validates confirmed autonomous assignees, enqueues work jobs, updates labels, writes comments, and records dispatched/skipped counts.
Periodic discovery task wiring
src/main.rs, src/dashboard/server.rs
Registers the supervisor module, starts a 12-second background discovery loop, integrates it into server startup, and tests empty-project execution.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DashboardServer
  participant Supervisor
  participant AgentflareMcp
  participant JobQueue
  DashboardServer->>Supervisor: start discovery loop
  Supervisor->>AgentflareMcp: query ready-for-work items
  Supervisor->>AgentflareMcp: validate assignee and update labels/comments
  Supervisor->>JobQueue: enqueue agentflare work job
  Supervisor-->>DashboardServer: return dispatched/skipped counts
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the summary and changes, but it omits the required Test plan and Notes for reviewers sections. Add the missing Test plan and Notes for reviewers sections, including concrete verification steps and any risk or compatibility notes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a supervisor auto-dispatch loop.
Docstring Coverage ✅ Passed Docstring coverage is 92.31% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/supervisor-auto-dispatch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 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/mcp_server.rs`:
- Around line 583-592: Update MspServer::for_test_memory to set
backend_project_link_override to a unique temporary project-link path, and use a
fixed test repository key if the constructor supports one. Ensure supervisor
ticks resolve against these test-only values without writing the checkout’s real
.agentflare/project.json or sharing paths across parallel tests.

In `@src/supervisor.rs`:
- Around line 41-57: The discovery flow in run_discovery_tick must preserve and
propagate database, project, and label lookup failures instead of converting
them into an empty result via .ok()? and the zero-count fallback. Replace the
erased errors with an error-returning path, update callers as needed, and have
the scheduler log failures while retaining the existing empty result only for a
valid absence of ready work.
- Around line 100-106: Update both label-transition paths in the supervisor,
including the NEEDS_MANUAL_LABEL flow and the corresponding dispatched flow, to
verify the destination label exists before removing ready-for-work. If the
destination label is missing, retain ready-for-work and report a configuration
error; otherwise add the destination label and remove the source label.
Alternatively, provision the required destination labels during setup.
- Around line 127-150: Update the dispatch flow surrounding queue.enqueue so the
item is reserved or otherwise made ineligible before enqueue is treated as
final, and only proceed when that reservation succeeds. Ensure failures removing
the ready-for-work label cannot leave the item eligible for a duplicate enqueue,
while preserving the existing dispatched-label and comment updates after
successful dispatch.
🪄 Autofix (Beta)

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 Plus

Run ID: 18eba06f-bb9b-48b5-a12f-fcc55945dcb0

📥 Commits

Reviewing files that changed from the base of the PR and between ddb0c67 and b013d3f.

📒 Files selected for processing (6)
  • crates/agentflare-backend/src/item.rs
  • src/dashboard/server.rs
  • src/main.rs
  • src/mcp_server.rs
  • src/mcp_server/item.rs
  • src/supervisor.rs

Comment thread src/mcp_server.rs
Comment on lines +583 to +592
/// Create an in-memory backend for tests that don't need a real repo/disk.
/// The other fields (skills, gateway, store, etc.) stay defaulted so they
/// lazily open `:memory:` SQLite connections or no-ops — no I/O to ~/.
#[cfg(test)]
pub(crate) fn for_test_memory() -> Self {
Self {
backend_db_override: Some(std::path::PathBuf::from(":memory:")),
..Default::default()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep memory-backed tests from writing the real repository link.

for_test_memory() leaves backend_project_link_override unset, so supervisor ticks call resolve_project() and write the checkout’s .agentflare/project.json. Parallel tests can race and tests leave repository state behind. Set a unique temporary project-link path (and preferably a fixed test repo key) here.

Proposed fix
 pub(crate) fn for_test_memory() -> Self {
+    let dir = tempfile::tempdir().expect("create test directory").keep();
     Self {
         backend_db_override: Some(std::path::PathBuf::from(":memory:")),
+        backend_project_link_override: Some(dir.join("project.json")),
+        backend_repo_key_override: Some("test-memory".to_string()),
         ..Default::default()
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Create an in-memory backend for tests that don't need a real repo/disk.
/// The other fields (skills, gateway, store, etc.) stay defaulted so they
/// lazily open `:memory:` SQLite connections or no-ops — no I/O to ~/.
#[cfg(test)]
pub(crate) fn for_test_memory() -> Self {
Self {
backend_db_override: Some(std::path::PathBuf::from(":memory:")),
..Default::default()
}
}
/// Create an in-memory backend for tests that don't need a real repo/disk.
/// The other fields (skills, gateway, store, etc.) stay defaulted so they
/// lazily open `:memory:` SQLite connections or no-ops — no I/O to ~/.
#[cfg(test)]
pub(crate) fn for_test_memory() -> Self {
let dir = tempfile::tempdir().expect("create test directory").keep();
Self {
backend_db_override: Some(std::path::PathBuf::from(":memory:")),
backend_project_link_override: Some(dir.join("project.json")),
backend_repo_key_override: Some("test-memory".to_string()),
..Default::default()
}
}
🤖 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/mcp_server.rs` around lines 583 - 592, Update MspServer::for_test_memory
to set backend_project_link_override to a unique temporary project-link path,
and use a fixed test repository key if the constructor supports one. Ensure
supervisor ticks resolve against these test-only values without writing the
checkout’s real .agentflare/project.json or sharing paths across parallel tests.

Comment thread src/supervisor.rs
Comment on lines +41 to +57
let fetched = mcp.with_backend_db(|conn| {
let project = mcp.resolve_project(conn).ok()?;
let labels = agentflare_backend::label::list_by_project(conn, &project.id).ok()?;
let mut label_id_by_name = std::collections::HashMap::new();
for l in &labels {
label_id_by_name.insert(l.name.clone(), l.id.clone());
}
let ready_id = label_id_by_name.get(READY_LABEL)?.clone();
let items = agentflare_backend::item::list_by_label(conn, &project.id, &ready_id).ok()?;
Some((items, label_id_by_name))
});

let Ok(Some((items, label_id_by_name))) = fetched else {
return result;
};
let Some(ready_id) = label_id_by_name.get(READY_LABEL).cloned() else {
return result;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not convert discovery failures into an empty tick.

Every fallible database/project/label operation is erased by .ok()?, and the outer match returns a zero-count result. The dashboard therefore cannot distinguish a broken backend from “no ready work,” silently disabling auto-dispatch. Return an error from run_discovery_tick and log it from the scheduler.

🤖 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 41 - 57, The discovery flow in
run_discovery_tick must preserve and propagate database, project, and label
lookup failures instead of converting them into an empty result via .ok()? and
the zero-count fallback. Replace the erased errors with an error-returning path,
update callers as needed, and have the scheduler log failures while retaining
the existing empty result only for a valid absence of ready work.

Comment thread src/supervisor.rs
Comment on lines +100 to +106
if let Some(needs_manual_id) = label_id_by_name.get(NEEDS_MANUAL_LABEL) {
let _ = mcp.item_add_label(ItemRequest {
action: "add_label".into(),
id: Some(item.id.clone()),
label_id: Some(needs_manual_id.clone()),
..Default::default()
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require destination labels before removing ready-for-work.

Both transitions remove the source label unconditionally but add needs-manual-dispatch/dispatched only when those labels already exist. A project with only ready-for-work configured loses the item from discovery without the promised status marker. Provision these labels or retain ready-for-work and report a configuration error.

Also applies to: 137-144

🤖 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 100 - 106, Update both label-transition paths
in the supervisor, including the NEEDS_MANUAL_LABEL flow and the corresponding
dispatched flow, to verify the destination label exists before removing
ready-for-work. If the destination label is missing, retain ready-for-work and
report a configuration error; otherwise add the destination label and remove the
source label. Alternatively, provision the required destination labels during
setup.

Comment thread src/supervisor.rs
Comment on lines +127 to +150
let Ok(info) = queue.enqueue(&job) else {
return false;
};

let _ = mcp.item_remove_label(ItemRequest {
action: "remove_label".into(),
id: Some(item.id.clone()),
label_id: Some(ready_id.to_string()),
..Default::default()
});
if let Some(dispatched_id) = label_id_by_name.get(DISPATCHED_LABEL) {
let _ = mcp.item_add_label(ItemRequest {
action: "add_label".into(),
id: Some(item.id.clone()),
label_id: Some(dispatched_id.clone()),
..Default::default()
});
}
let _ = mcp.comment_impl(CommentRequest {
action: "create".into(),
item_id: Some(item.id.clone()),
body: Some(format!("## supervisor — dispatched\n\njob: {}", info.id)),
..Default::default()
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

printf '\n## src/supervisor.rs\n'
sed -n '1,240p' src/supervisor.rs

printf '\n## queue-related symbols\n'
rg -n "enqueue\(|struct .*Queue|trait .*Queue|idempot|unique queue|dispatch|ready-for-work|DISPATCHED_LABEL" src -S

Repository: getappz/agentflare

Length of output: 19407


🏁 Script executed:

set -euo pipefail

printf '\n## queue implementation\n'
fd -a "queue.rs" . src
fd -a "mod.rs" src/agentflare_jobs src

printf '\n## enqueue and job identity\n'
rg -n "fn enqueue|enqueue\(&job\)|idempot|dedup|duplicate|job.id|struct AgentJob|impl AgentJob|new\(command" src -S

printf '\n## supervisor tests around repeated dispatch\n'
sed -n '240,340p' src/supervisor.rs

Repository: getappz/agentflare

Length of output: 16868


🏁 Script executed:

set -euo pipefail

printf '\n## crates/agentflare-jobs/src/queue.rs\n'
sed -n '1,260p' crates/agentflare-jobs/src/queue.rs

printf '\n## AgentJob definition\n'
rg -n "struct AgentJob|impl AgentJob|fn new\(" crates/agentflare-jobs/src -S

Repository: getappz/agentflare

Length of output: 10358


Make dispatch idempotent before treating enqueue as final. queue.enqueue() always inserts a new job with a fresh id, and there’s no reservation/idempotency key. If removing ready-for-work fails, the item remains eligible on the next tick and can be enqueued again.

🤖 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 127 - 150, Update the dispatch flow
surrounding queue.enqueue so the item is reserved or otherwise made ineligible
before enqueue is treated as final, and only proceed when that reservation
succeeds. Ensure failures removing the ready-for-work label cannot leave the
item eligible for a duplicate enqueue, while preserving the existing
dispatched-label and comment updates after successful dispatch.

@getappz
getappz merged commit 3ecbf95 into master Jul 30, 2026
16 checks passed
@getappz
getappz deleted the task/supervisor-auto-dispatch branch July 30, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant