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.
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 #492 — tools/pr/rebuild_f5.sh:80 on main has the identical git add -A — but it is worth recording next to #505 because:
after - before), so a file that *vanished* during the run is never reported. The "warn on return" half stays silent on exactly this case.git clean -fd because "a blanket git clean would take the session's own scratch notes with it". The rerere path in the same run already does something equivalent, by a different mechanism.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).
paused makes the wrap-up predicate unreachable for the session that owns the PRpoll.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).
git status silently inverts the sweep's safety propertybefore = 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.
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.
None.
No comments.