Skip to content

feat(item): comma-separated state_group filter + default-branch edit guard - #186

Merged
getappz merged 3 commits into
masterfrom
fix/item-list-state-group-filter
Jul 14, 2026
Merged

feat(item): comma-separated state_group filter + default-branch edit guard#186
getappz merged 3 commits into
masterfrom
fix/item-list-state-group-filter

Conversation

@getappz

@getappz getappz commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • item(list)'s state_group param now accepts a comma-separated list (e.g. "backlog,unstarted,started") instead of only a single exact value; /handoff's inbox grammar defaults to that open-states filter so completed/cancelled items don't show up unless the command explicitly says all.
  • PreToolUse now hard-blocks Write/Edit/NotebookEdit/ctx_patch/ctx_edit whenever the repo is checked out on its default branch (resolved via origin/HEAD, else main/master, else whatever's actually checked out), redirecting to create a worktree first.
  • Consolidated the git-shelling logic duplicated across mcp_server.rs, worktree.rs, hook_redirect.rs, gateway_integrations.rs, review.rs, and cli/review.rs into one src/git.rs module.

Test plan

  • cargo test --bin agentflare — 453 passed
  • cargo fmt --check
  • cargo clippy --bin agentflare — no new warnings

Summary by CodeRabbit

  • New Features
    • File-mutating tools are now blocked when running on the repository’s default branch; feature branches remain available for edits.
    • Item lists now accept multiple state_group values via comma-separated input.
    • Handoff inbox prompts now default to active items (while still honoring an explicit all option).
  • Bug Fixes
    • Improved reliability of current branch, default branch, and repository-location detection, resulting in more consistent review diffs and Git behavior.
    • More robust detection of whether a Git remote references GitHub.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR centralizes Git command handling, updates existing Git consumers, adds default-branch protection for mutating tools, and extends MCP item filtering and inbox prompt defaults to support multiple state groups.

Git workflows and branch protection

Layer / File(s) Summary
Shared Git primitives and validation
src/git.rs, src/main.rs
Adds centralized Git execution, branch/default-branch resolution, repository-root discovery, diff generation, and unit tests.
Git helper adoption across workflows
src/worktree.rs, src/review.rs, src/cli/review.rs, src/mcp_server.rs, src/gateway_integrations.rs
Routes existing Git operations through the shared helpers.
Default-branch mutation guard
src/hook_redirect.rs
Blocks configured mutating tools on resolved default branches while retaining existing redirect behavior and adding tests.

MCP inbox state-group filtering

Layer / File(s) Summary
Comma-separated state-group filtering
src/mcp_server.rs, src/mcp_server/item.rs
Documents and implements comma-separated state-group filtering for item listing, with multi-group test coverage.
Inbox handoff filtering guidance
src/mcp_prompts.rs
Defaults inbox prompts to open states and documents the explicit all behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant PreToolUse
  participant redirect_decision
  participant git
  participant classify
  PreToolUse->>redirect_decision: tool invocation
  redirect_decision->>git: resolve current and default branches
  git-->>redirect_decision: branch context
  redirect_decision->>classify: tool name and branch context
  classify-->>PreToolUse: redirect or branch denial
Loading

Possibly related PRs

Suggested labels: enhancement, rust

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: comma-separated item filtering and a default-branch edit guard.
Description check ✅ Passed The PR includes a solid Summary and Test plan; only the Notes for reviewers section is missing.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% 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 fix/item-list-state-group-filter

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hook_redirect.rs (1)

109-119: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Branch resolution runs on every tool call, not just mutating ones.

current_branch() and default_branch() are computed unconditionally inside the timed closure before classify ever checks whether tool_name is in MUTATING_TOOLS. Per src/hook.rs's pre_tool_use, redirect_decision runs on every PreToolUse invocation — so every Read, Bash, Grep, etc. call now spawns up to ~5 git subprocesses (1 for current_branch, up to 4 inside resolve_default_branch's fallback chain) purely to gate a check that only applies to a small fixed set of mutating tools.

⚡ Proposed fix — gate branch resolution behind the MUTATING_TOOLS check
     decide_with_timeout(GATING_TIMEOUT, move || {
-        let current = current_branch();
-        let default = default_branch();
+        let (current, default) = if MUTATING_TOOLS.contains(&tool_name.as_str()) {
+            (current_branch(), default_branch())
+        } else {
+            (None, None)
+        };
         let reason = classify(
             &tool_name,
             tool_input.as_ref(),
             (current.as_deref(), default.as_deref()),
         )?;
🤖 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/hook_redirect.rs` around lines 109 - 119, Update redirect_decision and
classify so tool mutability is checked before calling current_branch() or
default_branch(); return None immediately for tools outside MUTATING_TOOLS, and
resolve branch information only for mutating tools before continuing the
existing classification flow.
🤖 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/hook_redirect.rs`:
- Around line 64-73: Update branch_guard_reason_for to compare the branch only
with the resolved default branch. Remove the unconditional "main" and "master"
checks, while preserving the fallback behavior only when default is None if
resolution failure requires it; otherwise use the existing default value
directly.

---

Outside diff comments:
In `@src/hook_redirect.rs`:
- Around line 109-119: Update redirect_decision and classify so tool mutability
is checked before calling current_branch() or default_branch(); return None
immediately for tools outside MUTATING_TOOLS, and resolve branch information
only for mutating tools before continuing the existing classification flow.
🪄 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: 51797363-3068-4b7d-9593-62ac9789b3e0

📥 Commits

Reviewing files that changed from the base of the PR and between 5ece9c3 and da93ccc.

📒 Files selected for processing (9)
  • src/cli/review.rs
  • src/gateway_integrations.rs
  • src/git.rs
  • src/hook_redirect.rs
  • src/main.rs
  • src/mcp_prompts.rs
  • src/mcp_server.rs
  • src/review.rs
  • src/worktree.rs

Comment thread src/hook_redirect.rs
getappz added a commit that referenced this pull request Jul 14, 2026
…dundant name check

Two issues from CodeRabbit review on PR #186:
- redirect_decision resolved current/default branch unconditionally on every
  PreToolUse call, spawning up to ~5 git subprocesses per Read/Bash/Grep/etc,
  not just the handful of mutating tools that actually need the check.
- branch_guard_reason_for compared the branch against the resolved default
  AND unconditionally against literal "main"/"master", which could false-flag
  a branch happening to be named "master" in a repo whose real default has
  since moved to something else. Now only falls back to guessing main/master
  when default resolution genuinely failed.
getappz added 3 commits July 15, 2026 02:11
…inbox default

item(list)'s state_group param now accepts a comma-separated list (e.g.
"backlog,unstarted,started") instead of only a single exact value. /handoff's
inbox grammar defaults to that open-states filter so completed/cancelled
items don't show up unless the command explicitly says `all`.
…shelling

PreToolUse now hard-blocks Write/Edit/NotebookEdit/ctx_patch/ctx_edit whenever
the repo is checked out on its default branch (resolved via origin/HEAD, else
main/master, else whatever's actually checked out), redirecting the agent to
create a worktree first. The branch/tool decision is injectable so tests never
depend on which branch this repo itself happens to be on.

Also pulls the git-shelling logic that had been copy-pasted across
mcp_server.rs, worktree.rs, hook_redirect.rs, gateway_integrations.rs,
review.rs, and cli/review.rs into one src/git.rs module (run_in/run_in_opt/
run_in_ok/current_branch/resolve_default_branch/repo_toplevel/diff), so every
caller shares one implementation instead of five near-identical copies.
…dundant name check

Two issues from CodeRabbit review on PR #186:
- redirect_decision resolved current/default branch unconditionally on every
  PreToolUse call, spawning up to ~5 git subprocesses per Read/Bash/Grep/etc,
  not just the handful of mutating tools that actually need the check.
- branch_guard_reason_for compared the branch against the resolved default
  AND unconditionally against literal "main"/"master", which could false-flag
  a branch happening to be named "master" in a repo whose real default has
  since moved to something else. Now only falls back to guessing main/master
  when default resolution genuinely failed.
@getappz
getappz force-pushed the fix/item-list-state-group-filter branch from fa83553 to 0086228 Compare July 14, 2026 20:43

@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.

🧹 Nitpick comments (1)
src/mcp_server/item.rs (1)

66-132: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Comma-separated state_group filter looks correct.

group.split(',').map(str::trim).collect() plus wanted.contains(...) correctly matches any of the listed groups and stays backward-compatible with single-value filters; verified against the new multi-group test in mcp_server.rs.

One optional gap: an unrecognized/misspelled group value (e.g. "backlgo") silently yields an empty result rather than an error, since unwrap_or(false) excludes non-matching items with no feedback. This mirrors the prior single-value behavior, so it's not a regression, but a small validation step against the documented set (backlog|unstarted|started|completed|cancelled|triage) would surface typos instead of a confusing empty list.

♻️ Optional validation
+const VALID_STATE_GROUPS: &[&str] = &["backlog", "unstarted", "started", "completed", "cancelled", "triage"];
+
 if let Some(group) = &req.state_group {
     let wanted: Vec<&str> = group.split(',').map(str::trim).collect();
+    if let Some(bad) = wanted.iter().find(|g| !VALID_STATE_GROUPS.contains(g)) {
+        return Err(ErrorData::invalid_params(
+            format!("unknown state_group '{bad}' — expected one of backlog|unstarted|started|completed|cancelled|triage"),
+            None,
+        ));
+    }
     items.retain(|i| {
🤖 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 66 - 132, Optionally validate each
trimmed value in req.state_group against the documented groups backlog,
unstarted, started, completed, cancelled, and triage before filtering in
item_list. Return an invalid-params ErrorData for any unrecognized value;
preserve the existing comma-separated matching behavior for valid groups.
🤖 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.

Nitpick comments:
In `@src/mcp_server/item.rs`:
- Around line 66-132: Optionally validate each trimmed value in req.state_group
against the documented groups backlog, unstarted, started, completed, cancelled,
and triage before filtering in item_list. Return an invalid-params ErrorData for
any unrecognized value; preserve the existing comma-separated matching behavior
for valid groups.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ab56b2b4-801d-48d6-8b18-b6629b6a319b

📥 Commits

Reviewing files that changed from the base of the PR and between fa83553 and 0086228.

📒 Files selected for processing (10)
  • src/cli/review.rs
  • src/gateway_integrations.rs
  • src/git.rs
  • src/hook_redirect.rs
  • src/main.rs
  • src/mcp_prompts.rs
  • src/mcp_server.rs
  • src/mcp_server/item.rs
  • src/review.rs
  • src/worktree.rs
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/mcp_prompts.rs
  • src/main.rs
  • src/cli/review.rs
  • src/gateway_integrations.rs
  • src/review.rs
  • src/git.rs
  • src/worktree.rs
  • src/hook_redirect.rs

@getappz
getappz merged commit 1ca98ef into master Jul 14, 2026
14 checks passed
@getappz
getappz deleted the fix/item-list-state-group-filter branch July 14, 2026 20:49
getappz added a commit that referenced this pull request Jul 18, 2026
…refix, fix flaky nanoid test

Review of task/184 (item #186) found the PR didn't actually cover its own
motivating case: item_get -- the exact call from #184's bug report
(item(get, id="178")) -- was never wired to resolve_item_id, and
resolve_id's numeric parse didn't strip a leading '#', so #-prefixed
sequence ids silently fell through to the UUID passthrough branch instead
of resolving, despite both the commit message and the tool's own schema
description claiming that support.

- item_get now resolves through resolve_item_id, closing the original gap
- resolve_id strips a leading '#' before parsing as numeric
- Added the tests #184 explicitly required and that were missing: bare
  numeric, #-prefixed numeric, not-found numeric, project-scoped lookup,
  end-to-end via the item MCP tool
- Fixed a flaky test: handoff_tool_requires_recipient_and_assigns_item
  asserted a filename via item_id.to_lowercase(), which doesn't match
  production's actual AgentflareMcp::slugify() transform -- slugify also
  collapses '_' to '-', which to_lowercase() doesn't, so the assertion
  failed whenever a randomly-generated nanoid id happened to contain an
  underscore. Now asserts against the real transform.

cargo test --workspace (632 passing, 0 failed), cargo fmt --check, and
cargo clippy --workspace --all-features -D warnings all clean.
getappz added a commit that referenced this pull request Jul 18, 2026
…tion to nanoid (#257)

* item/claim: accept numeric sequence_id or #-prefixed id; switch id generation to nanoid

item/claim MCP tools (get, update, update_state, delete, claim, heartbeat,
release, done, add_label, remove_label; claim's target param) now accept
either a UUID or a numeric sequence_id (bare or #-prefixed), resolved via
agentflare_backend::item::resolve_id scoped to the repo's linked project.
Not-found numeric ids return the same not-found shape as an unmatched UUID.
Closes #184.

Also switches db_kit::ids::new_id() from uuid::Uuid::now_v7() to
nanoid::nanoid!(), updating every caller across agentflare-artifacts and
agentflare-backend (asset/comment/label/project/state/webhook/workspace).

cargo build --workspace --all-features, cargo test --workspace (630+
passing across the bin plus every crate), cargo fmt --check, and cargo
clippy --workspace --all-features -D warnings all clean (the one remaining
clippy hit is the pre-existing Windows-only agent_launch.rs test import,
tracked separately as item #169, unrelated to this change).

* item/claim: wire item_get through sequence_id resolution, support #-prefix, fix flaky nanoid test

Review of task/184 (item #186) found the PR didn't actually cover its own
motivating case: item_get -- the exact call from #184's bug report
(item(get, id="178")) -- was never wired to resolve_item_id, and
resolve_id's numeric parse didn't strip a leading '#', so #-prefixed
sequence ids silently fell through to the UUID passthrough branch instead
of resolving, despite both the commit message and the tool's own schema
description claiming that support.

- item_get now resolves through resolve_item_id, closing the original gap
- resolve_id strips a leading '#' before parsing as numeric
- Added the tests #184 explicitly required and that were missing: bare
  numeric, #-prefixed numeric, not-found numeric, project-scoped lookup,
  end-to-end via the item MCP tool
- Fixed a flaky test: handoff_tool_requires_recipient_and_assigns_item
  asserted a filename via item_id.to_lowercase(), which doesn't match
  production's actual AgentflareMcp::slugify() transform -- slugify also
  collapses '_' to '-', which to_lowercase() doesn't, so the assertion
  failed whenever a randomly-generated nanoid id happened to contain an
  underscore. Now asserts against the real transform.

cargo test --workspace (632 passing, 0 failed), cargo fmt --check, and
cargo clippy --workspace --all-features -D warnings all clean.
getappz pushed a commit that referenced this pull request Aug 25, 2026
Discovery tick dispatches purely on the ready-for-work label, so items
#184/#185/#186/#187 (go/no-go candidates from #166's spec) whose own
description says "Decision pending — not dispatched" got auto-dispatched
and re-dispatched across multiple agents anyway -- the prose was never
actually enforced.

Add a needs-decision label that blocks run_discovery_tick even while
ready-for-work is also present. Stripping ready-for-work alone wouldn't
have been durable: redispatch unconditionally re-attaches it, so the new
label has to keep gating on its own until a human clears it.

Agentflare-Agent: claude-code
Agentflare-Branch: fix/dispatch-failure-ceiling-any-reason
getappz pushed a commit that referenced this pull request Aug 25, 2026
find_duplicate_pr searches for any open PR carrying the item's "for item
#N" marker, with no way to tell "a fresh dispatch about to redundantly
open a second PR" apart from "a self-repair job reclaiming its own item's
existing worktree/branch, whose entire job is to push a fix onto that
exact PR." The latter hit the same short-circuit, bailed with "needs
human review" without ever attempting a repair, and released the claim --
which only clears assignee_agent, never restores the state group, so the
item was left orphaned in "started" with no label either
run_discovery_tick or run_review_sweep would ever revisit (reproduced
live on item #186/PR #597, whose CI stayed red with no further attempts).

Exclude a still-open PR whose head branch matches the current worktree's
branch from counting as a duplicate at all -- it's this job's own PR, not
a competing one. A merged match still always short-circuits regardless of
branch, since that's this check's other job: self-heal an item whose PR
landed while its tracked state fell out of sync (items #122/#156).

Agentflare-Agent: claude-code
Agentflare-Branch: fix/dispatch-failure-ceiling-any-reason
getappz added a commit that referenced this pull request Aug 27, 2026
…ent dispatch (#619)

WorkflowEngine::recover() resumes non-terminal sdd_loop runs after a daemon
restart by calling execute_workflow() directly, bypassing execute_work's
run_in_worktree/EXECUTE_WORK_CWD_LOCK entirely (PR #601 only guards fresh
dispatches from the job queue). A resumed run's agent dispatch still went
through the ambient-cwd run_headless, so it silently inherited whatever
worktree another concurrently-running item's chdir happened to have set --
confirmed live on items #186/#187, both binding into task/186's worktree.

Thread the item's own worktree path through StepInvocation.cwd (persisted
on WorkItemData, read at step-execution time so a resumed run still has
it) and dispatch via run_headless_in instead, mirroring the pattern
app_send_hook already uses for App workflows. This removes the dependency
on global process cwd for this call path rather than trying to widen the
lock to cover the recovery path too.

Also splits the execute_work_impl dispatch-fixture tests out of work.rs
into work_dispatch_fixture_tests.rs to stay under the file's LOC gate.

Agentflare-Agent: claude-code
Agentflare-Branch: task/191-opencode-agentflare-work-dispatch-doesn
Agentflare-Item: 191
Agentflare-Session: 0be92ce2-29ad-46d0-9303-597ede893b7b

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