Skip to content

feat: agent/model router - task-attribute routing, agentflare work integration, config.toml loader - #380

Merged
getappz merged 6 commits into
masterfrom
task/419
Aug 3, 2026
Merged

getappz merged 6 commits into
masterfrom
task/419

Conversation

@getappz

@getappz getappz commented Aug 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

Closes the missing fourth piece described in item #419: given a claimed task, decide which installed agent should run it.

  • crates/agent-registry/src/router.rs: TaskContext, RouteDecision, RouterRule/RuleMatch, RouterConfig, and route() — precedence is explicit assignment > first matching rule with an installed preference > installed default > None (caller falls back, e.g. cede). Named TaskContext/RouteDecision rather than reusing optimize::runtime::Router/RouteContext (a different, prompt-text-based per-call model nudge) to avoid an unrelated, riskier rename.
  • agentflare work: --agent is now optional. Given explicitly, behavior is unchanged. Omitted, the item is claimed first and then routed from its own attributes (labels, metadata.size, metadata.kind, and critically assignee_agent — previously ignored entirely).
  • agent_registry::parse_router_config: reads the [router] table (default + [[router.rule]] entries) out of ~/.agentflare/config.toml, ignoring every other top-level table so it composes cleanly once feat: migrate artifact storage onto agentflare-store documents+blobs + vent: origin routing, throttle, batched filing, judge-prompt hook #331 (unified config) lands. Unknown agent names are dropped rather than failing the whole parse; malformed TOML falls back to the empty config with a warning, never a crash.
  • mcp_server::item::parsed_kind: same metadata JSON-blob convention as the existing parsed_size, free-form rather than a fixed set. Factored the shared double-encoding-safe unwrap into parsed_metadata.

Deliberately out of scope

  • TaskContext.repo — no field maps to it; items don't carry an originating-repo concept in single-project-per-repo agentflare today.
  • Dashboard surfacing of RouteDecision.reason — no consumer needed it yet.
  • Full #331 config surface — this only reads its own [router] corner.

Test plan

  • cargo test -p agentflare-agent-registry (39 tests) — router + config parsing
  • cargo test --bin agentflare cli::work:: (18 tests) — agent resolution / auto-routing
  • cargo test --bin agentflare metadata_field_tests:: (5 tests) — parsed_kind/parsed_size
  • cargo test --bin agentflare item_groom — no regression from the metadata-parsing refactor
  • cargo check --workspace
  • cargo clippy --all-targets -- -A unsafe_code -A clippy::pedantic -D warnings

Summary by CodeRabbit

  • New Features
    • Added configurable task routing based on assignments, labels, task type, size, repository, and installed agents.
    • The work command can automatically select an agent when none is specified.
    • Added support for routing rules and default agents in the local configuration file.
    • Added support for resolving agent aliases and canonical names.
  • Bug Fixes
    • Improved handling of task metadata, including encoded values and invalid or missing fields.
    • Invalid or unsupported agent selections now release claimed work and provide an error.
    • Configuration problems are reported as warnings without preventing work from continuing.

getappz added 4 commits August 3, 2026 23:58
Adds the missing fourth piece from item #419: given a claimed task
(labels/kind/size) and the set of installed agents, decide which one
should run it. RouteDecision always carries a reason. Precedence:
explicit assignment > first matching rule with an installed
preference > default (only if installed) > None (caller falls back,
e.g. cede).

Named TaskRouter/TaskContext rather than reusing optimize::runtime's
Router/RouteContext, which is a different, prompt-text-based nudge for
per-call model routing -- kept as is to avoid an unrelated rename.

Scoped to the decision function only. Loading rules from
~/.agentflare/config.toml is left to #331 (unified config, still in
progress) so this doesn't duplicate that surface ahead of time.
Wires agent_registry::route into 'agentflare work' as its first real
consumer. --agent still wins when given (fails fast before claiming,
same as before). Omit it and the item is claimed first, then routed
from its own attributes via a TaskContext (labels, metadata.size, and
critically assignee_agent) against the installed-agent set from
detect_all_with.

RouterConfig::default() has no rules yet, so today this only resolves
when the item itself already names an assignee -- work.rs previously
ignored that field entirely and always required a manual --agent. Rule-
based routing from ~/.agentflare/config.toml activates automatically
once #331 lands, with no further change here.

Widens mcp_server::item::parsed_size to pub(crate) to reuse the
existing (and already metadata-double-encoding-safe) size parser
instead of duplicating it.
Adds agent_registry::parse_router_config: reads a config.toml's
[router] table (default agent + [[router.rule]] entries with a
labels/kind/size 'when' and a use preference list, single string or
array) into a RouterConfig. Every other top-level table in the file
is ignored -- this only ever reads its own corner, so it composes
cleanly once #331 (unified config) actually owns the file.

An agent name that doesn't resolve (typo, or unknown to this build's
registry) is dropped from wherever it appeared rather than failing
the whole parse -- route() already treats a missing preference as
'try the next one', so a bad name behaves like one that's merely not
installed. Only malformed TOML is a hard Err.

Wires it into 'agentflare work': load_router_config() reads the file
(missing file -> the empty config, parse error -> a warning + the
empty config, never a crash) and resolve_agent() now takes it instead
of a hardcoded RouterConfig::default(). Auto-routing (--agent
omitted) can now match a labels/kind/size rule, not just an item's
own assignee_agent.

Adds the toml crate (0.8, already resolved transitively in the
lockfile) as agent-registry's first non-clap/serde dependency -- TOML
parsing is exactly the kind of already-solved-by-a-library problem
worth reaching for one over a hand-rolled subset parser for.
Adds mcp_server::item::parsed_kind, same metadata-blob convention as
the existing parsed_size ({\kind\: \locate\}) but free-form rather
than a fixed S/M/L set, since task shape isn't a small enumerable
list. Factors the shared double-encoding-safe JSON unwrap the two
now have in common out into parsed_metadata so the workaround lives
in exactly one place.

resolve_agent's TaskContext.kind was hardcoded to None (no item field
mapped to it yet) -- now reads parsed_kind, so a
'[[router.rule]] when.kind = \...\' rule can actually match once
something sets that metadata key. Nothing writes it yet (no kind
UI/tool surface exists), same bootstrapping state size was in before
groom's UI grew a way to set it.

7 new tests (5 for parsed_kind/parsed_size directly, 2 for a
kind-matching rule through resolve_agent); moved the new test module
to the end of item.rs after clippy flagged items-after-test-module
from the pre-existing top-of-file placement.
@coderabbitai

coderabbitai Bot commented Aug 3, 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: 0085d772-61f4-48e6-811d-0dd5934ab622

📥 Commits

Reviewing files that changed from the base of the PR and between 30410c8 and 315d19c.

📒 Files selected for processing (5)
  • crates/agent-registry/src/lib.rs
  • crates/agent-registry/src/registry.rs
  • crates/agent-registry/src/router.rs
  • src/cli/work.rs
  • src/mcp_server/item.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/agent-registry/src/lib.rs
  • crates/agent-registry/src/router.rs
  • src/mcp_server/item.rs
  • src/cli/work.rs

📝 Walkthrough

Walkthrough

Adds TOML-based task routing to agent-registry and integrates automatic agent selection into agentflare work. Selection uses explicit assignments, item metadata, configured rules, and installed agents.

Changes

Agent routing

Layer / File(s) Summary
Router configuration and precedence
crates/agent-registry/Cargo.toml, crates/agent-registry/src/lib.rs, crates/agent-registry/src/registry.rs, crates/agent-registry/src/router.rs
Adds TOML parsing, public routing types, alias resolution, agent filtering, routing precedence, and tests.
Item metadata extraction
src/mcp_server/item.rs
Adds shared parsing for plain and double-encoded metadata, including validated kind and size values.
Work command agent resolution
src/cli/work.rs
Makes --agent optional and resolves agents from explicit input, assignments, metadata, router rules, and installed agents. Failed routing releases the claim and reports an error.

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

Sequence Diagram(s)

sequenceDiagram
  participant WorkCommand
  participant ItemService
  participant AgentRouter
  participant HeadlessLauncher
  WorkCommand->>ItemService: fetch claimed item labels and metadata
  ItemService-->>WorkCommand: return assignment and item context
  WorkCommand->>AgentRouter: route task context and installed agents
  AgentRouter-->>WorkCommand: return resolved agent or no decision
  WorkCommand->>HeadlessLauncher: start work with resolved agent
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes task-attribute routing, work integration, and config.toml loading.
Description check ✅ Passed The description includes a detailed summary and completed test plan; the optional reviewer notes section is omitted.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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/419

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: 3

🧹 Nitpick comments (6)
crates/agent-registry/src/router.rs (2)

77-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Agent-name resolution is duplicated and inconsistent across three entry points. agent_by_str in crates/agent-registry/src/router.rs and agent_by_name in src/cli/work.rs are the same exact-id registry lookup, and neither applies agent_registry::canonicalize. Only the item-assignee path canonicalizes. As a result, the alias "claude" resolves from assignee_agent but fails from --agent claude and from default = "claude" in ~/.agentflare/config.toml. Export one canonicalizing resolver from agent-registry and use it at every site.

  • crates/agent-registry/src/router.rs#L77-L82: apply crate::registry::canonicalize inside agent_by_str, and make the resolver pub so callers do not copy it.
  • src/cli/work.rs#L189-L192: replace the local agent_by_name with the exported resolver so the explicit --agent value accepts the same aliases as assignee_agent, and apply it to the pre-claim check at lines 272-274 as well.
🤖 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 `@crates/agent-registry/src/router.rs` around lines 77 - 82, The agent-name
resolver must canonicalize aliases consistently across all entry points. In
crates/agent-registry/src/router.rs lines 77-82, make agent_by_str public and
apply crate::registry::canonicalize before the registry lookup; in
src/cli/work.rs lines 189-192, remove the local agent_by_name and use the
exported resolver for explicit --agent values and the pre-claim check at lines
272-274.

179-191: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify that the assignment branch bypasses the installed check.

The doc states installed is "the sole authority on availability". The assignment branch returns before any installed check, so route can return an agent that is not installed. The run then fails later in run_headless with NotFound. Either state this exception in the doc, or gate the assignment on installed so all three paths behave the same.

🤖 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 `@crates/agent-registry/src/router.rs` around lines 179 - 191, The route
function’s explicit task assignment currently bypasses the installed-agent
availability rule. Update the documentation above route to explicitly state that
assigned agents are accepted without checking installed, or instead update the
assignment branch in route to verify the assigned agent exists in installed and
fall through when it does not; keep the behavior consistent with the chosen
contract.
src/cli/work.rs (3)

344-346: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Remove the expect by resolving the explicit agent once.

Line 346 relies on the pre-claim check at lines 272-274 using identical resolution logic. If either site changes, for example to add canonicalize, this line panics after the item is already claimed, and the claim is not released. Resolve the explicit name once before the claim and reuse the Agent value.

🔧 Proposed fix
-    if let Some(explicit) = args.agent.as_deref()
-        && agent_by_name(explicit).is_none()
-    {
-        crate::ui::error(&format!(
-            "unknown agent: {explicit} — use `agentflare agents list`"
-        ));
-        return 1;
-    }
+    let explicit_agent = match args.agent.as_deref() {
+        Some(explicit) => match agent_by_name(explicit) {
+            Some(agent) => Some(agent),
+            None => {
+                crate::ui::error(&format!(
+                    "unknown agent: {explicit} — use `agentflare agents list`"
+                ));
+                return 1;
+            }
+        },
+        None => None,
+    };

Then at the resolution site:

-    let agent_enum = if let Some(explicit) = args.agent.as_deref() {
-        agent_by_name(explicit).expect("checked above before claiming")
-    } else {
+    let agent_enum = if let Some(agent) = explicit_agent {
+        agent
+    } else {
🤖 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/cli/work.rs` around lines 344 - 346, Update the agent-resolution flow in
the work command to resolve the explicit agent name once before claiming the
item, store the resulting Agent value, and reuse it for the post-claim selection
in the agent_enum block. Remove the agent_by_name(...).expect(...) call there
while preserving route-based resolution when no explicit agent is provided.

158-162: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Warn on read errors that are not "file missing".

The let Ok(text) = ... else arm treats every read failure as "no config". A permission error or non-UTF-8 file then produces silent default routing, and the user sees only "pass --agent explicitly". Match on ErrorKind::NotFound for the silent path and warn for the rest, the same way the parse error path warns.

🔧 Proposed fix
-    let Ok(text) = std::fs::read_to_string(&path) else {
-        return agent_registry::RouterConfig::default();
+    let text = match std::fs::read_to_string(&path) {
+        Ok(text) => text,
+        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
+            return agent_registry::RouterConfig::default();
+        }
+        Err(e) => {
+            crate::ui::warning(&format!(
+                "{}: could not read config ({e}) — ignoring, pass --agent explicitly",
+                path.display()
+            ));
+            return agent_registry::RouterConfig::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/cli/work.rs` around lines 158 - 162, Update load_router_config to
distinguish std::fs::read_to_string errors by ErrorKind: return the default
RouterConfig silently only for NotFound, and emit a warning for all other read
failures before returning the default, consistent with the existing parse-error
handling.

348-374: 🚀 Performance & Scalability | 🔵 Trivial

Consider the cold-start cost of detect_all_with while the claim is held, and print the route reason.

Two observations, neither blocking:

  1. detect_all_with probes every Tier::Cli registry entry on PATH and runs each binary with its version args. The item is already claimed at this point. On a cold version_cache, one slow agent binary delays the claimed run up to the detection timeout. The cache limits this to first runs, so measure before changing anything.
  2. resolve_agent discards RouteDecision::reason. Line 374 prints only the agent name. Returning and printing the reason makes an unexpected pick self-explaining. Dashboard display stays out of scope.
🤖 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/cli/work.rs` around lines 348 - 374, Measure the cold-start duration of
agent_registry::detect_all_with while the claim is held before changing its
placement or behavior. Update the resolve_agent flow to retain the RouteDecision
reason alongside the selected agent, then print that reason with the agent in
the existing output near headless_args. Keep dashboard display and other routing
behavior unchanged.
src/mcp_server/item.rs (1)

1039-1043: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a double-encoded size test.

parsed_size now reads through the shared parsed_metadata helper. Only parsed_kind has double-encoding coverage. Add the size case so a future change to parsed_metadata cannot silently break the original workaround.

💚 Proposed test
     #[test]
     fn parsed_size_still_rejects_values_outside_s_m_l() {
         assert_eq!(parsed_size(r#"{"size":"XL"}"#), None);
         assert_eq!(parsed_size(r#"{"size":"M"}"#), Some("M".to_string()));
     }
+
+    #[test]
+    fn parsed_size_survives_double_encoded_metadata() {
+        let metadata = double_encoded(serde_json::json!({"size": "S"}));
+        assert_eq!(parsed_size(&metadata), Some("S".to_string()));
+    }
🤖 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/item.rs` around lines 1039 - 1043, Extend the parsed_size test
coverage around parsed_size to include a double-encoded size payload, matching
the existing parsed_kind workaround coverage. Assert that the helper still
extracts the expected size value from the double-encoded input while preserving
the current rejection and normal-value assertions.
🤖 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 `@crates/agent-registry/src/router.rs`:
- Around line 4-8: Correct the module comment in router.rs to describe the
existing free route function and RouterConfig/RouterRule types instead of
referring to a nonexistent TaskRouter type. Preserve the distinction from
optimize::runtime::Router while using only symbols actually defined in the
module.
- Around line 52-59: Update the kind and size checks in the rule-matching
expression to compare configured and task values with case-insensitive equality,
while preserving the existing behavior for unset filters and the labels_ok
logic.

In `@src/cli/work.rs`:
- Around line 14-19: Update the doc comment for the agent field in the work
command so it no longer claims user-configured routing rules are unavailable or
dependent on issue `#331`. Describe automatic routing as using the claimed item’s
assignee_agent and configured router rules, while preserving the existing
--agent behavior.

---

Nitpick comments:
In `@crates/agent-registry/src/router.rs`:
- Around line 77-82: The agent-name resolver must canonicalize aliases
consistently across all entry points. In crates/agent-registry/src/router.rs
lines 77-82, make agent_by_str public and apply crate::registry::canonicalize
before the registry lookup; in src/cli/work.rs lines 189-192, remove the local
agent_by_name and use the exported resolver for explicit --agent values and the
pre-claim check at lines 272-274.
- Around line 179-191: The route function’s explicit task assignment currently
bypasses the installed-agent availability rule. Update the documentation above
route to explicitly state that assigned agents are accepted without checking
installed, or instead update the assignment branch in route to verify the
assigned agent exists in installed and fall through when it does not; keep the
behavior consistent with the chosen contract.

In `@src/cli/work.rs`:
- Around line 344-346: Update the agent-resolution flow in the work command to
resolve the explicit agent name once before claiming the item, store the
resulting Agent value, and reuse it for the post-claim selection in the
agent_enum block. Remove the agent_by_name(...).expect(...) call there while
preserving route-based resolution when no explicit agent is provided.
- Around line 158-162: Update load_router_config to distinguish
std::fs::read_to_string errors by ErrorKind: return the default RouterConfig
silently only for NotFound, and emit a warning for all other read failures
before returning the default, consistent with the existing parse-error handling.
- Around line 348-374: Measure the cold-start duration of
agent_registry::detect_all_with while the claim is held before changing its
placement or behavior. Update the resolve_agent flow to retain the RouteDecision
reason alongside the selected agent, then print that reason with the agent in
the existing output near headless_args. Keep dashboard display and other routing
behavior unchanged.

In `@src/mcp_server/item.rs`:
- Around line 1039-1043: Extend the parsed_size test coverage around parsed_size
to include a double-encoded size payload, matching the existing parsed_kind
workaround coverage. Assert that the helper still extracts the expected size
value from the double-encoded input while preserving the current rejection and
normal-value assertions.
🪄 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: ae78dfea-63f1-4598-9b44-d5b4f766aeb4

📥 Commits

Reviewing files that changed from the base of the PR and between db12cee and 30410c8.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • crates/agent-registry/Cargo.toml
  • crates/agent-registry/src/lib.rs
  • crates/agent-registry/src/router.rs
  • src/cli/work.rs
  • src/mcp_server/item.rs

Comment thread crates/agent-registry/src/router.rs Outdated
Comment thread crates/agent-registry/src/router.rs Outdated
Comment thread src/cli/work.rs
- Dedupe agent-name resolution: export agent_registry::agent_by_name
  (canonicalize + registry lookup) as the one place every agent-name
  string resolves through, and use it in router.rs's TOML parsing and
  work.rs's --agent/assignee_agent paths. Previously only the
  assignee_agent path canonicalized, so '--agent claude' and
  'default = \claude\' in config.toml silently failed where
  assignee_agent=\claude\ worked.
- Remove the work.rs .expect() after claiming: resolve_agent is now
  called once, post-claim, for both the explicit and auto-routed
  paths, instead of a separate pre-claim lookup whose result was
  assumed (rather than proven) to still hold after the claim.
- load_router_config now only treats ErrorKind::NotFound as 'no
  config yet'; any other read error (permissions, etc.) warns instead
  of silently degrading to the empty config.
- RuleMatch's kind/size comparison is now case-insensitive, matching
  the labels comparison it sits next to instead of diverging from it.
- resolve_agent now returns (Agent, reason) and agentflare work prints
  the reason alongside the agent name, so an auto-routed pick is
  self-explaining instead of just a bare id.
- Doc fixes: router.rs's module comment no longer references a
  TaskRouter type that was never actually created; route()'s doc now
  states explicitly (with the rationale) that explicit assignment is
  the one path that bypasses the installed check; WorkArgs's --agent
  doc no longer claims config.toml rules are gated on #331 landing
  (the loader shipped in this same PR).
- Adds a double-encoded-metadata test for parsed_size (parsed_kind
  already had one) and a test proving explicit assignment survives
  route() even when the assigned agent isn't in installed.

Addresses all 3 actionable comments and the RouteDecision.reason
nitpick from CodeRabbit's review; the detect_all_with cold-start-cost
observation is left as-is per its own 'measure before changing'
caveat, and dashboard display of the reason stays out of scope.
@getappz

getappz commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@getappz
getappz merged commit 687f304 into master Aug 3, 2026
17 checks passed
@getappz
getappz deleted the task/419 branch August 3, 2026 19:36
@getappz
getappz restored the task/419 branch August 3, 2026 19:37
@getappz
getappz deleted the task/419 branch August 5, 2026 13:48
getappz pushed a commit that referenced this pull request Aug 11, 2026
…t blocker prior sessions on this item reported.

**What I implemented** (case 3 from the scope-broadening note — a dispatched agent correctly finding it's blocked on an unmet dependency, making zero changes, but with no way to say so distinct from "genuinely nothing to do"):

- `build_prompt` (`src/cli/work.rs`) now teaches the agent an `AGENTFLARE_HOLD: <reason>` reply marker for exactly that case.
- `execute_work`'s success path checks the reply for that marker (`detect_hold_signal`) *before* ever calling `item_done`. If present, it calls `item_release` instead — claim released, item state untouched, no `mark_completed` — posts a distinct "on hold" comment, and exits 0. This sidesteps the deeper root cause I found while investigating: `branch_diverged` diffs the *whole* branch against target, so a branch already carrying commits from an earlier claim attempt reads as "diverged" even when the current run changed nothing, silently routing a legitimate hold into the real-completion path (that's what actually happened to item #91).
- Added 4 new tests; also confirmed case 2 (failed commit) is already correctly fixed by this branch's prior commit — `nothing_was_ever_committed`'s release path never touches item state.
- Verified: `cargo build` clean, `cargo test --bin agentflare cli::work::` — 39 passed (4 new). `cargo clippy -D warnings` fails workspace-wide on a pre-existing, unrelated lint in `crates/agent-registry/src/router.rs` (last touched in PR #380, untouched here) — confirmed pre-existing on unmodified HEAD too.

**Why it's uncommitted:** this session's own `.worktrees/task/92/.git` is mounted read-only (`mount` confirms `ro`), and the `mcp__flare__item done` daemon fallback also failed — `attempt to write a readonly database`. Same dual-blocked state the prior dispatch of this exact item hit. The one changed file sits uncommitted:

- `src/cli/work.rs` (+83/-1)

A human or unsandboxed process needs to `git add`/`git commit`/`git push` it, or re-run `mcp__flare__item done` for item 92 once the read-only mounts lift.

Agentflare-Branch: task/92
Agentflare-Item: 92
getappz pushed a commit that referenced this pull request Aug 12, 2026
…t blocker prior sessions on this item reported.

**What I implemented** (case 3 from the scope-broadening note — a dispatched agent correctly finding it's blocked on an unmet dependency, making zero changes, but with no way to say so distinct from "genuinely nothing to do"):

- `build_prompt` (`src/cli/work.rs`) now teaches the agent an `AGENTFLARE_HOLD: <reason>` reply marker for exactly that case.
- `execute_work`'s success path checks the reply for that marker (`detect_hold_signal`) *before* ever calling `item_done`. If present, it calls `item_release` instead — claim released, item state untouched, no `mark_completed` — posts a distinct "on hold" comment, and exits 0. This sidesteps the deeper root cause I found while investigating: `branch_diverged` diffs the *whole* branch against target, so a branch already carrying commits from an earlier claim attempt reads as "diverged" even when the current run changed nothing, silently routing a legitimate hold into the real-completion path (that's what actually happened to item #91).
- Added 4 new tests; also confirmed case 2 (failed commit) is already correctly fixed by this branch's prior commit — `nothing_was_ever_committed`'s release path never touches item state.
- Verified: `cargo build` clean, `cargo test --bin agentflare cli::work::` — 39 passed (4 new). `cargo clippy -D warnings` fails workspace-wide on a pre-existing, unrelated lint in `crates/agent-registry/src/router.rs` (last touched in PR #380, untouched here) — confirmed pre-existing on unmodified HEAD too.

**Why it's uncommitted:** this session's own `.worktrees/task/92/.git` is mounted read-only (`mount` confirms `ro`), and the `mcp__flare__item done` daemon fallback also failed — `attempt to write a readonly database`. Same dual-blocked state the prior dispatch of this exact item hit. The one changed file sits uncommitted:

- `src/cli/work.rs` (+83/-1)

A human or unsandboxed process needs to `git add`/`git commit`/`git push` it, or re-run `mcp__flare__item done` for item 92 once the read-only mounts lift.

Agentflare-Branch: task/92
Agentflare-Item: 92
getappz added a commit that referenced this pull request Aug 12, 2026
…_done (#455)

* fix(item): distinguish a failed commit from nothing-to-commit in item_done

commit_uncommitted previously swallowed a failed `git commit` (e.g. a
read-only .git, a rejected pre-commit hook) with no way for item_done
to tell that apart from a genuinely clean tree. A dispatched job could
exit 0 with real work correctly staged but never committed, and the
item would silently release as if nothing needed doing.

commit_uncommitted now reports a commit failure distinctly from
"nothing staged"; item_done treats a failed commit as a hard failure
(comment + do not release-as-clean) instead of folding it into the
nothing-was-ever-committed path.

The new regression test moved into its own file
(item_commit_failure_tests.rs) rather than growing action_tests.rs
past its 1500-line LOC gate.

Agentflare-Agent: claude-code
Agentflare-Branch: task/92
Agentflare-Item: 92

* Implementation is done and verified, but I've hit the same environment blocker prior sessions on this item reported.

**What I implemented** (case 3 from the scope-broadening note — a dispatched agent correctly finding it's blocked on an unmet dependency, making zero changes, but with no way to say so distinct from "genuinely nothing to do"):

- `build_prompt` (`src/cli/work.rs`) now teaches the agent an `AGENTFLARE_HOLD: <reason>` reply marker for exactly that case.
- `execute_work`'s success path checks the reply for that marker (`detect_hold_signal`) *before* ever calling `item_done`. If present, it calls `item_release` instead — claim released, item state untouched, no `mark_completed` — posts a distinct "on hold" comment, and exits 0. This sidesteps the deeper root cause I found while investigating: `branch_diverged` diffs the *whole* branch against target, so a branch already carrying commits from an earlier claim attempt reads as "diverged" even when the current run changed nothing, silently routing a legitimate hold into the real-completion path (that's what actually happened to item #91).
- Added 4 new tests; also confirmed case 2 (failed commit) is already correctly fixed by this branch's prior commit — `nothing_was_ever_committed`'s release path never touches item state.
- Verified: `cargo build` clean, `cargo test --bin agentflare cli::work::` — 39 passed (4 new). `cargo clippy -D warnings` fails workspace-wide on a pre-existing, unrelated lint in `crates/agent-registry/src/router.rs` (last touched in PR #380, untouched here) — confirmed pre-existing on unmodified HEAD too.

**Why it's uncommitted:** this session's own `.worktrees/task/92/.git` is mounted read-only (`mount` confirms `ro`), and the `mcp__flare__item done` daemon fallback also failed — `attempt to write a readonly database`. Same dual-blocked state the prior dispatch of this exact item hit. The one changed file sits uncommitted:

- `src/cli/work.rs` (+83/-1)

A human or unsandboxed process needs to `git add`/`git commit`/`git push` it, or re-run `mcp__flare__item done` for item 92 once the read-only mounts lift.

Agentflare-Branch: task/92
Agentflare-Item: 92

* fix: rustfmt + avoid world-writable permissions in test cleanup

Restore the git dir's exact original permissions after the
readonly-simulation test instead of a blanket set_readonly(false),
which clippy correctly flags as making the path world-writable on
Unix.

Agentflare-Agent: claude-code
Agentflare-Branch: task/92
Agentflare-Item: 92

* fix(flare-git-core): avoid world-writable permissions in test cleanup

Same clippy::permissions_set_readonly_false issue as the mcp_server
test fixed in the prior commit -- restore exact original permissions
instead of a blanket set_readonly(false) toggle.

worktree.rs's test module moved to a sibling worktree_tests.rs (pure
code motion, no behavior change) since the fix pushed it past the
1500-line LOC gate -- item #92's own diff had already brought the
file to the limit before this fix's few extra lines tipped it over.

Agentflare-Agent: claude-code
Agentflare-Branch: task/92
Agentflare-Item: 92

* test: skip readonly-git-dir commit-failure tests on Windows

FILE_ATTRIBUTE_READONLY on a directory doesn't block writes to files
inside it on Windows (unlike Unix permission bits), so the readonly-
git-dir simulation these two tests rely on to force a commit failure
doesn't hold there -- the commit silently succeeds instead, failing
the test's own assertion. Confirmed as the actual cause of PR 455's
Windows CI failure (auto-commit succeeded when the test expected an
error). Unix-only, matching this repo's existing convention for
platform-specific filesystem-permission tests.

Agentflare-Agent: claude-code
Agentflare-Branch: task/92
Agentflare-Item: 92

---------

Co-authored-by: shiva <shiva@gosysinfo.tech>
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