fix(rearm): exclude an abandoned restore's residue from the new baseline#106
Conversation
|
Warning Review limit reached
Next review available in: 27 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
WalkthroughAdds a patch-diff parser ( ChangesStale Restore Residue Exclusion
Estimated code review effort: 3 (Moderate) | ~30 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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 `@src/bmad_loop/verify.py`:
- Around line 1554-1561: The created-path handling in verify.py is only
stripping the b/ prefix, so mnemonic prefixes like w/ and 2/ still end up
recorded in new_files. Update the path normalization in the branch that
processes the +++ header to strip all known new-side prefixes before adding the
path, using the existing _DIFF_ABSENT check and the target parsing logic around
creating/new_files.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 686c9edd-3adb-4703-98f1-02aeb59c6bec
📒 Files selected for processing (8)
CHANGELOG.mdsrc/bmad_loop/cli.pysrc/bmad_loop/runs.pysrc/bmad_loop/verify.pytests/test_cli.pytests/test_portability_guard.pytests/test_runs.pytests/test_verify.py
Re-arming a story whose previous re-drive had already applied a restore patch snapshotted that patch's new (untracked) files as pre-existing, so every later rollback preserved them and finalize_commit's `add -A` swept the abandoned attempt into the corrected story's commit. rearm_escalation now captures the old latch/baseline before the unconditional overwrite, parses the stale patch's creations (verify.patch_new_files) and subtracts them from the refreshed baseline_untracked. Nothing is deleted here — the re-drive's own reset removes what the snapshot stops blessing. Deliberately not a `git apply -R`: it fails on any drift and misbehaves on the committed variant. Commits the escalated attempt left below the advanced baseline share their range with the resolve session's own blessed commits, so they cannot be reverted mechanically: they are journaled and echoed to stderr for the human to classify. Best-effort throughout — a missing or unreadable patch degrades to the pre-fix behavior rather than wedging the resolve. Closes #90
patch_new_files only stripped the standard b/ prefix, but the restore patch is saved by the skill session under the user's git config — diff.mnemonicPrefix=true emits w/ (and --no-index emits 2/), leaving a wrong path in the exclusion set so the #90 fix silently no-ops. apply_patch runs plain `git apply` (default -p1), which strips the first component whatever it is; the parser now does the same. Targets -p1 cannot strip (no slash, e.g. --no-prefix output) are skipped: that apply failed outright, so no residue exists — recording them verbatim could exclude (and later delete) a same-named file the human created.
6a59590 to
0c70428
Compare
Closes #90
Origin: the PR #86 final review, which traced this chain link-by-link. #86's restore latch supplies the untracked payload; #78's re-arm baseline machinery supplies the absorption. Tracked hunks were always safe — this is untracked-files-only.
The bug
A restore re-drive escalates after the patch applied (session error / CRITICAL / exhausted-mid-redrive / pre-commit veto / commit-fail — all PAUSE without a rollback). The human re-resolves and re-arms from scratch, and
rearm_escalationrefreshestask.baseline_untrackedfrom the current tree — capturing the applied patch's new files as "pre-existing". Every later rollback then preserves them, andfinalize_commit'sadd -Asweeps the abandoned attempt into the corrected story's commit. The only remaining defense was step-01's prose dirty-tree check: a probabilistic LLM judgment, exactly the guarantee class the restore design refuses elsewhere.The fix
rearm_escalationcapturesold_latch/old_baselinebefore the unconditional latch overwrite, then between the spec block and the snapshot refresh subtracts the stale patch's creations from the refreshedbaseline_untracked.verify.patch_new_files(patch_path)— text-parses the saved patch (--- /dev/nullin adiff --gitblock marks a creation). The caller has moved the tree on, sogit apply --numstatcan't be trusted to still apply. Deletions are never returned, and every ambiguous entry (quoted paths, renames, header-lookalike hunk content) is skipped rather than guessed: under-reporting degrades to today's behavior, over-reporting gets a user's file deleted.git apply -R, per the issue's alternative. Re-arm already advances the baseline to HEAD and the re-drive'ssafe_rollbackcleans uncommitted tracked edits;apply -Rwould fail on any drift and misbehaves on the committed variant. Nothing is deleted insiderearm— the existing reset machinery removes the now-unblessed paths at re-drive time.git applywould hit "already exists").stale-restore-commitsnames the shas and the human classifies.stale-restore-unparseableand degrades to the pre-fix snapshot. It must never raiseRearmError— a deleted patch file cannot be allowed to wedgeresolve.New journal events
stale-restore-excluded/-unparseable/-commits, each echoed as one stderr line fromcmd_resolve(the commits warning is the only notice the human ever gets)._DIFF_ABSENThoists git's/dev/nulldiff token to one acked constant;verify.pyjoins the portability guard'sPATH_ALLOWfor it, since git spells the absent side/dev/nullon Windows too.Verification
trunk checkclean.tests/test_runs.py::_escalated_rungainsgit_project=True— a real repo, so the snapshot refresh actually runs (in a baretmp_pathits best-effortexceptswallowed every git call and the refresh silently no-opped). Five re-arm tests: residue excluded, re-latch still excludes, missing patch degrades loudly and still warns about commits, no latch → no events, commits above the old baseline →stale-restore-commits. Sevenpatch_new_filesunit tests, plus a CLI test that the echo fires for this re-arm's events only.runs.pychange.bmad-loop resolveCLI on a seeded escalation:newfile.txtdropped from the refreshed snapshot whilehuman.txtand the latched patch survive; a follow-upsafe_rollbackat the re-drive's baseline then removesnewfile.txtand keepshuman.txt.Summary by CodeRabbit