Skip to content

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

Merged
getappz merged 6 commits into
masterfrom
task/92
Aug 12, 2026
Merged

getappz merged 6 commits into
masterfrom
task/92

Conversation

@getappz

@getappz getappz commented Aug 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

Closes #92.

Test plan

  • cargo test -p flare-git-core --lib — 151 passed
  • cargo test -p agentflare --bin agentflare mcp_server::tests:: — 141 passed (includes new item_done_reports_a_hard_error_when_auto_commit_fails_on_a_dirty_tree)
  • New regression test moved into its own file rather than growing action_tests.rs past the 1500-line LOC gate

Summary by CodeRabbit

  • New Features
    • Added support for placing items on hold when work is blocked by an external dependency.
    • Hold reasons are recorded, optionally notified, and the item is released without being marked complete.
  • Bug Fixes
    • Improved automatic commit handling when completing items.
    • Commit failures now show a clear error, preserve uncommitted work, and keep the item incomplete.
    • Clean worktrees and successful commits continue to complete normally.
  • Tests
    • Added coverage for commit failures and hold-signal handling.

@coderabbitai

coderabbitai Bot commented Aug 11, 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
📝 Walkthrough

Walkthrough

The Git worktree API now returns explicit commit outcomes. MCP item completion preserves failed worktrees and claims. Successful work runs can emit an AGENTFLARE_HOLD: signal to defer item completion.

Changes

Commit outcome handling

Layer / File(s) Summary
Git commit outcome contract and implementation
crates/flare-git-core/src/worktree.rs, src/worktree.rs
commit_uncommitted now returns CommitOutcome for clean, successful, and failed operations. Tests cover each outcome.
Item completion failure path
src/mcp_server/item.rs, src/mcp_server/tests/item_commit_failure_tests.rs, src/mcp_server/tests/mod.rs
item_done reports commit failures, preserves incomplete work, and validates the behavior with an integration test.

Hold signal workflow

Layer / File(s) Summary
Hold signal protocol and execution path
src/cli/work.rs
The prompt defines the AGENTFLARE_HOLD: protocol. Successful runs with a nonempty hold reason release the item, post an on-hold comment, optionally notify a recipient, and skip item_done. Tests cover parsing and prompt instructions.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR addresses commit-failure handling, but linked issue #92 requires a skill registry and MCP skill tools. Relink this PR to the issue for commit-failure handling, or implement the skill registry requirements from issue #92.
Out of Scope Changes check ⚠️ Warning The commit-failure and AGENTFLARE_HOLD changes are unrelated to the skill registry requirements in issue #92. Remove the unrelated changes or split them into separate PRs linked to issues that cover their objectives.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change to distinguish failed commits from clean worktrees in item_done.
Description check ✅ Passed The description includes a clear summary and test results, but it omits the template's Notes for reviewers section.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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/92

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

🤖 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/flare-git-core/src/worktree.rs`:
- Around line 1254-1270: Replace the permission-based write protection with an
index lock: at both affected sites,
crates/flare-git-core/src/worktree.rs:1254-1270 and
src/mcp_server/tests/item_commit_failure_tests.rs:51-72, create
<git_dir>/index.lock before invoking commit_uncommitted or item_done, then
remove the lock afterward so cleanup can proceed. Remove the set_readonly logic;
both sites require this direct change.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c4d92750-c0b6-498e-ae90-e9a3e4882788

📥 Commits

Reviewing files that changed from the base of the PR and between 5df7697 and 496b394.

📒 Files selected for processing (5)
  • crates/flare-git-core/src/worktree.rs
  • src/mcp_server/item.rs
  • src/mcp_server/tests/item_commit_failure_tests.rs
  • src/mcp_server/tests/mod.rs
  • src/worktree.rs

Comment thread crates/flare-git-core/src/worktree.rs Outdated
Comment on lines +1254 to +1270
// Make the index unwritable so `git add -A` fails deterministically
// while `git status` (read-only) still succeeds.
let index_path = worktree_path.join(".git");
let git_dir = std::fs::read_to_string(&index_path)
.ok()
.and_then(|s| s.strip_prefix("gitdir: ").map(str::trim).map(String::from))
.map(PathBuf::from)
.unwrap_or(index_path);
let mut perms = std::fs::metadata(&git_dir).unwrap().permissions();
perms.set_readonly(true);
std::fs::set_permissions(&git_dir, perms.clone()).unwrap();

let result = commit_uncommitted(&item, &repo.path, "auto-committed");

// Restore write permission so the temp dir can be cleaned up.
perms.set_readonly(false);
std::fs::set_permissions(&git_dir, perms).unwrap();

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

🧩 Analysis chain

🌐 Web query:

Does Rust std::fs::Permissions::set_readonly(true) reliably prevent creating files in a directory on Windows, and how does the Windows directory read-only attribute behave?

💡 Result:

No, rust std::fs::Permissions::set_readonly(true) does not reliably prevent the creation of files within a directory on Windows [1][2]. The behavior of the read-only attribute on Windows is fundamentally different for directories than it is for files [1][3]. While the FILE_ATTRIBUTE_READONLY flag prevents writing to or deleting a specific file, it is generally ignored by Windows for directories [3][4]. On modern versions of Windows, setting this attribute on a directory does not restrict file creation, renaming, or deletion within that directory [1][3][4]. Historically, on Windows 7 and earlier, this attribute could prevent the deletion of empty directories, but it did not prevent modification of their contents [1][5]. Modern Windows components, including Windows Explorer and most applications, ignore the read-only attribute when performing file system operations on directories [6][4]. In some contexts, Windows Explorer may treat the read-only or system attribute on a folder as a signal to check for a desktop.ini file for folder customization, rather than as a security restriction [4]. To control whether a user or process can create files in a directory on Windows, you must use Windows Access Control Lists (ACLs) [7]. Access rights, such as FILE_ADD_FILE (to permit file creation) or FILE_ADD_SUBDIRECTORY (to permit subdirectory creation), are managed through the security descriptor associated with the directory [8][7]. The read-only attribute is a legacy file system flag and is not an effective mechanism for enforcing security or write-access policies on directories [3][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target implementation ---'
sed -n '1215,1290p' crates/flare-git-core/src/worktree.rs

printf '%s\n' '--- affected integration test ---'
sed -n '1,100p' src/mcp_server/tests/item_commit_failure_tests.rs

printf '%s\n' '--- relevant symbols and lock-file handling ---'
rg -n --glob '*.rs' 'commit_uncommitted|index\.lock|git add|add\(-A\)|Command::new\("git"\)' crates src

Repository: getappz/agentflare

Length of output: 12697


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- commit_uncommitted implementation ---'
sed -n '700,780p' crates/flare-git-core/src/worktree.rs

printf '%s\n' '--- worktree path and git-directory helpers ---'
rg -n -A35 -B15 'fn (worktree_path|git_dir|resolve_worktree|status|run_git)|gitdir' crates/flare-git-core/src/worktree.rs

printf '%s\n' '--- remainder of integration test ---'
sed -n '90,180p' src/mcp_server/tests/item_commit_failure_tests.rs

printf '%s\n' '--- Git availability ---'
git --version

Repository: getappz/agentflare

Length of output: 12537


🏁 Script executed:

#!/bin/bash
set -eu

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
repo="$tmp/repo"
wt="$tmp/worktree"

git init -q -b master "$repo"
git -C "$repo" config user.email test@example.com
git -C "$repo" config user.name test
git -C "$repo" commit --allow-empty -qm initial
git -C "$repo" worktree add -q "$wt" -b task/test

printf '%s\n' '--- worktree git metadata ---'
cat "$wt/.git"
git_dir="$(sed 's/^gitdir: //' "$wt/.git")"
printf 'git_dir=%s\n' "$git_dir"

printf '%s\n' 'change' > "$wt/changed.txt"
printf '%s\n' '--- status before index.lock ---'
git -C "$wt" status --short

: > "$git_dir/index.lock"
printf '%s\n' '--- status with index.lock ---'
status_rc=0
git -C "$wt" status --short || status_rc=$?
printf 'status_exit=%s\n' "$status_rc"

printf '%s\n' '--- add with index.lock ---'
add_rc=0
git -C "$wt" add -A >"$tmp/add.stdout" 2>"$tmp/add.stderr" || add_rc=$?
printf 'add_exit=%s\n' "$add_rc"
cat "$tmp/add.stdout"
cat "$tmp/add.stderr"

Repository: getappz/agentflare

Length of output: 827


Use an index lock to force git add failure.

Permissions::set_readonly(true) does not block directory writes on Windows. Both tests can let git add -A succeed and fail at unwrap_err().

Create <git_dir>/index.lock before commit_uncommitted or item_done, then remove it afterward at both affected sites.

📍 Affects 2 files
  • crates/flare-git-core/src/worktree.rs#L1254-L1270 (this comment)
  • src/mcp_server/tests/item_commit_failure_tests.rs#L51-L72
🤖 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/flare-git-core/src/worktree.rs` around lines 1254 - 1270, Replace the
permission-based write protection with an index lock: at both affected sites,
crates/flare-git-core/src/worktree.rs:1254-1270 and
src/mcp_server/tests/item_commit_failure_tests.rs:51-72, create
<git_dir>/index.lock before invoking commit_uncommitted or item_done, then
remove the lock afterward so cleanup can proceed. Remove the set_readonly logic;
both sites require this direct change.

@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/cli/work.rs`:
- Around line 832-837: Update the detect_hold_signal handling around
mcp.item_release to validate both the call result and the response’s released
field before posting the hold comment or returning success; on an error or
released == false, do not report the hold or exit successfully. Add a test
covering a failed release.
- Around line 832-837: Update the detect_hold_signal handling around
item_release to verify the worktree is clean before releasing the claim. If the
worktree is dirty, treat the run as failed and retain the claim; only invoke
item_release for a clean worktree, preserving the existing hold-signal behavior
otherwise.
- Around line 838-847: Apply cap_reply_for_comment to reply_text before
constructing the hold comment in the work hold branch, and use the capped
content for both comment creation and notify. Add a test covering an oversized
reply in the hold path to verify the persisted and notified content is capped.
- Around line 240-244: Update detect_hold_signal to inspect only the final
nonempty reply line, require that line to begin at byte zero with
AGENTFLARE_HOLD:, and return its nonempty trimmed reason; otherwise return None.
Add a regression test covering an earlier protocol-like line followed by normal
completion text so the item is released through item_done.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0928de87-6fa3-4652-810a-00ca82b3ba7b

📥 Commits

Reviewing files that changed from the base of the PR and between 496b394 and 1b6bf3f.

📒 Files selected for processing (1)
  • src/cli/work.rs

Comment thread src/cli/work.rs
Comment on lines +240 to +244
fn detect_hold_signal(reply: &str) -> Option<&str> {
reply.lines().find_map(|line| {
let reason = line.trim().strip_prefix("AGENTFLARE_HOLD:")?.trim();
(!reason.is_empty()).then_some(reason)
})

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

Restrict hold detection to the final protocol line.

Line 240 accepts a matching line anywhere in the reply. Line 242 also accepts leading whitespace. This differs from the prompt requirement to end the reply with a line that starts exactly with AGENTFLARE_HOLD:.

A normal reply that mentions the protocol before its completion summary will release the item and bypass item_done. Inspect only the final nonempty line, require the prefix at byte zero, and add a regression test.

🤖 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 240 - 244, Update detect_hold_signal to inspect
only the final nonempty reply line, require that line to begin at byte zero with
AGENTFLARE_HOLD:, and return its nonempty trimmed reason; otherwise return None.
Add a regression test covering an earlier protocol-like line followed by normal
completion text so the item is released through item_done.

Comment thread src/cli/work.rs
Comment on lines +832 to +837
if let Some(reason) = detect_hold_signal(&reply_text) {
let _ = mcp.item_release(ItemRequest {
action: "release".into(),
id: Some(item_id.into()),
..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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not report a hold when claim release fails.

The result from mcp.item_release is discarded. item_release can return an error or a successful response with "released": false. This path then posts an on-hold comment and exits with code zero while the item can remain actively claimed.

Validate both the call result and the released field before posting the hold comment or returning success. Add a test for a failed release.

🤖 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 832 - 837, Update the detect_hold_signal
handling around mcp.item_release to validate both the call result and the
response’s released field before posting the hold comment or returning success;
on an error or released == false, do not report the hold or exit successfully.
Add a test covering a failed release.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject hold signals from a dirty worktree.

The prompt requests no file changes, but this path does not enforce that condition. item_release attempts cleanup, refuses to remove a dirty worktree, and still releases the claim. A run that modifies files and emits AGENTFLARE_HOLD: leaves an unclaimed dirty worktree for a later dispatch.

Check that the worktree is clean before release. If it is dirty, treat the run as a failure and preserve the claim for recovery.

🤖 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 832 - 837, Update the detect_hold_signal
handling around item_release to verify the worktree is clean before releasing
the claim. If the worktree is dirty, treat the run as failed and retain the
claim; only invoke item_release for a clean worktree, preserving the existing
hold-signal behavior otherwise.

Comment thread src/cli/work.rs
Comment on lines +838 to +847
let comment_body =
format!("## agentflare work — on hold\n\n{reason}\n\n{reply_text}");
let _ = mcp.comment_impl(CommentRequest {
action: "create".into(),
item_id: Some(item_id.into()),
body: Some(comment_body.clone()),
..Default::default()
});
if let Some(recipient) = args.notify.as_deref() {
notify(recipient, &comment_body, item_id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Apply the reply-size cap before creating a hold comment.

This branch persists and notifies the full reply_text. The ordinary completion path uses cap_reply_for_comment at Line 876 to prevent large raw agent output from expanding comment storage and notification payloads.

Use cap_reply_for_comment here as well. Add a hold-path test with an oversized reply.

🤖 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 838 - 847, Apply cap_reply_for_comment to
reply_text before constructing the hold comment in the work hold branch, and use
the capped content for both comment creation and notify. Add a test covering an
oversized reply in the hold path to verify the persisted and notified content is
capped.

shiva added 3 commits August 12, 2026 13:36
…_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
…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
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
shiva and others added 3 commits August 12, 2026 13:48
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
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
@getappz
getappz merged commit c905c38 into master Aug 12, 2026
16 checks passed
@getappz
getappz deleted the task/92 branch August 12, 2026 08:55
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