feat(item): comma-separated state_group filter + default-branch edit guard - #186
Conversation
📝 WalkthroughWalkthroughChangesThe 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
MCP inbox state-group filtering
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
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 winBranch resolution runs on every tool call, not just mutating ones.
current_branch()anddefault_branch()are computed unconditionally inside the timed closure beforeclassifyever checks whethertool_nameis inMUTATING_TOOLS. Persrc/hook.rs'spre_tool_use,redirect_decisionruns on every PreToolUse invocation — so everyRead,Bash,Grep, etc. call now spawns up to ~5 git subprocesses (1 forcurrent_branch, up to 4 insideresolve_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
📒 Files selected for processing (9)
src/cli/review.rssrc/gateway_integrations.rssrc/git.rssrc/hook_redirect.rssrc/main.rssrc/mcp_prompts.rssrc/mcp_server.rssrc/review.rssrc/worktree.rs
…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.
…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.
fa83553 to
0086228
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/mcp_server/item.rs (1)
66-132: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winComma-separated
state_groupfilter looks correct.
group.split(',').map(str::trim).collect()pluswanted.contains(...)correctly matches any of the listed groups and stays backward-compatible with single-value filters; verified against the new multi-group test inmcp_server.rs.One optional gap: an unrecognized/misspelled group value (e.g.
"backlgo") silently yields an empty result rather than an error, sinceunwrap_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
📒 Files selected for processing (10)
src/cli/review.rssrc/gateway_integrations.rssrc/git.rssrc/hook_redirect.rssrc/main.rssrc/mcp_prompts.rssrc/mcp_server.rssrc/mcp_server/item.rssrc/review.rssrc/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
…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.
…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.
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
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
…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>
Summary
item(list)'sstate_groupparam now accepts a comma-separated list (e.g."backlog,unstarted,started") instead of only a single exact value;/handoff'sinboxgrammar defaults to that open-states filter so completed/cancelled items don't show up unless the command explicitly saysall.PreToolUsenow hard-blocksWrite/Edit/NotebookEdit/ctx_patch/ctx_editwhenever 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.mcp_server.rs,worktree.rs,hook_redirect.rs,gateway_integrations.rs,review.rs, andcli/review.rsinto onesrc/git.rsmodule.Test plan
cargo test --bin agentflare— 453 passedcargo fmt --checkcargo clippy --bin agentflare— no new warningsSummary by CodeRabbit
state_groupvalues via comma-separated input.alloption).