PR #505 follow-ups: rerere's `git add -A` eats untracked files, and `paused` blocks wrap-up nf-f6qa ← Beads

closed priority 2 task unassigned

Twinned from https://github.com/cfsh/ninefold/issues/512 by tools/beads/import_github.py's reconcile pass.

Suggestions from ninefold-reviewer's review of #505. All advisory — none blocked that PR, which is CONVERGED. Filed unclaimed.

1. The rerere auto-resolve path deletes the session's untracked files (pre-existing, and the same family as #503)

merge_branch runs git add -A && git commit --no-edit when rerere replays a recorded resolution — while parked on f5/integration. -A stages untracked, non-ignored files too, so every scratch file the session happens to be holding is committed into the f5 merge commit. hand_back then does git checkout <session-branch>, and git removes those paths from the working tree because they are tracked in HEAD (f5) and absent from the target branch.

Net effect: any untracked file present when the rebuild starts is silently destroyed if any branch in the frontier hits a rerere-replayed conflict. That path is not rare — #505's own test-plan output shows ✓ chore/co-located-tests (conflict auto-resolved from rerere) on a live run — and CLAUDE.md's merge_pr.sh section says outright that "agent clones accumulate scratch files as a matter of course".

This is not introduced by #505 or #492tools/pr/rebuild_f5.sh:80 on main has the identical git add -A — but it is worth recording next to #505 because:

Possible shapes: stash untracked before the merge loop and restore after; git add -u plus an explicit add of the conflicted paths only (git diff --name-only --diff-filter=U at the point of conflict); or the worktree option on #503, which closes it structurally along with everything else.

Related: #465 (the same script cannot converge on a delete/modify conflict).

2. paused makes the wrap-up predicate unreachable for the session that owns the PR

poll.py's termination check is gaps_pointers == 0 and tracked_open == 0 and not mine (tools/pr/poll.py:904). A paused PR stays OPEN indefinitely by design, so whichever session has it in tracked-prs.txt counts it as live work forever and can never reach WRAP_UP — the state the label exists to describe ("not awaiting action") is precisely the state the predicate reads as awaiting action. Live today: #485 is paused + rev:escalated.

Either exclude paused from tracked_open, or document that a paused PR should be dropped from tracked-prs.txt (which then orphans it from every poll — the caveat CLAUDE.md already records for archived sessions).

3. A failed baseline git status silently inverts the sweep's safety property

before = untracked_paths(git) (rebuild_f5.py, in rebuild) goes through git.raw(..., check=False), which returns an empty set on failure. The sweep's whole argument is after - before with both reads on the same tree; with an empty baseline, every untracked .uid / .import in the tree — including ones the session already owned and intended to commit — falls into created and is deleted.

test_a_failed_status_reads_as_nothing_untracked justifies check=False for the *sweep-side* read, where degrading to "nothing untracked" is safely inert. On the *baseline* read the same degradation points the other way: silently destructive rather than silently permissive. Blast radius is bounded to regenerable sidecars, which is why this is a hardening note rather than a defect — but the two call sites want opposite failure behaviour from one helper.

4. An all-paused queue announces "No open PRs"

If every open PR carries paused, open_pr_branches returns [] and rebuild prints → No open PRs — f5/integration = main (#400's path). The lines above it do say what happened, so nothing is hidden — but the line is literally false, and this tool's own header treats "reports something other than what it did" as the failure it exists to prevent. → No unpaused PRs would cost one branch.

Dependencies

None.

Comments

No comments.

Add a comment