fix(repo-hygiene): validate clean-batch plan records against requested --tier (#1081) - #1088
Conversation
…d --tier (#1081) clean-batch.sh --apply dispatched purely on each plan line's REPO/GITDIR kind; the apply-time --tier flag was informational only, so a stale or swapped plan executed its full gated content while the banner named a narrower tier (a --tier build dry-run plan applied with --tier caches removed both bin/ and .pytest_cache/ while printing Tier: caches). Apply now pre-scans the plan and refuses it atomically (usage error, exit 2, nothing removed, no apply banner) when it carries a record the requested tier does not authorize: a build REPO record under --tier caches, a caches record under build, or a GITDIR record under a non-git tier. The removal set is unchanged; this closes the scope-misrepresentation between --tier and what apply actually removes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude encountered an error after 1s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
🔒 babysit-prs lane claiming this PR for on-branch fix work this cycle (amendment-round: 16, safe tier). Will fix clear branch-owned findings and push; will not resolve threads or merge (safe tier). |
Resolves the plugins/repo-hygiene/CHANGELOG.md conflict: both sides bumped 0.7.0 -> 0.7.1, main with a documentation-only entry that is already published. This branch's behavioral fix therefore takes 0.7.2 so consumers already on the published 0.7.1 still receive it; main's 0.7.1 entry is preserved verbatim.
|
Claude finished @kyle-sexton's task in 2m 27s —— View job PR Review — fix(repo-hygiene): validate clean-batch plan records against requested --tier (#1081)
SummaryThe fix is correct and well-designed. The Findings1. Test gap: The documentation at
The code correctly handles it — The actual logic path: pre-scan reads 2. PR description version note is stale The PR body says Cosmetic only — the changelog and manifest agree, so this does not affect release tooling. 3. Minor: pre-scan silently skips unrecognized REPO tokens [[ "$b" == caches || "$b" == build ]] || continueA REPO record with an unrecognized token (e.g. What's working well
Finding 1 (test gap) is worth addressing before merge since the docs explicitly call out the |
|
Claude finished @kyle-sexton's task in 2m 46s —— View job Security Review — PR #1088
No security issues found. The five changed files are: Authorization logic (
|
|
Claude finished @kyle-sexton's task in 1m 58s —— View job PR Review — fix(repo-hygiene): validate clean-batch plan records against requested --tier (#1081)
SummaryThe fix is correct and structurally sound. Finding 1 — Test gap:
|
|
Claude finished @kyle-sexton's task in 1m 52s —— View job Security Review — PR #1088
No security issues found. The five changed files are Tier-authorization logic (
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f699fdb84f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
# Conflicts: # plugins/repo-hygiene/CHANGELOG.md
|
Claude encountered an error after 2s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 2s —— View job I'll analyze this and get back to you. |
|
Warning Automated security review did not complete — this is an infrastructure failure, not a review verdict. Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."
Re-running the job, or pushing a new commit, will retry the review. |
|
Warning Automated review did not complete — this is an infrastructure failure, not a review verdict. Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."
Re-running the job, or pushing a new commit, will retry the review. |
… --tier all `all` authorizes REPO and GITDIR records, so a narrower plan cleared every per-record authorization test and then ran only half the tier: a `build` plan (no GITDIR records) skipped every prune while the banner printed `Tier: all`, and a `git` plan (no REPO records) skipped every build removal. The same scope misrepresentation the tier pre-scan exists to stop, approached from the other side. A non-empty plan applied with `--tier all` must now carry both kinds. An empty plan removes nothing under any tier and stays a no-op.
|
Claude encountered an error after 2s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 2s —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 98fbe7064a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…batch --tier all The `all` both-kinds-present pre-scan counted a record by kind alone, so a truncated `GITDIR` line naming no representative worktree satisfied the git half of a build plan: apply printed `Tier: all` and `gitdirs=1` while performing no Git cleanup and exiting 0. The apply loop's GITDIR arm had no record validation either — an empty representative counted toward `gitdirs=` and then read as `! -d`, reporting a store that "vanished after dry-run" for a prune that never had a target. Presence is now satisfied only by a structurally well-formed record, and the apply loop fails a malformed GITDIR closed as structural corruption, mirroring the REPO arm. Both sites share one field-shape predicate per kind so pre-scan and apply cannot drift on what a record is. Manifest existence stays out of the predicate: a manifest that vanished after the dry-run is a per-record runtime failure, not a wrong-tier plan, and judging it in the pre-scan would refuse the whole apply with the wrong diagnosis. The REPO mirror is fixed in the same pass — a `REPO` line naming no manifest no longer satisfies the build half either — so a `git` plan carrying one now refuses atomically (exit 2, nothing removed) instead of pruning and exiting 1. The refusal message says "no well-formed GITDIR/REPO record" rather than "no record", which a user looking at a plan file that visibly carries the line would misread.
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
Summary
clean-batch.sh --apply --batch-plan <path>dispatched purely on each plan line'skind (
REPO/GITDIR); the apply-time--tierflag was informational only (it setthe banner and whether the summary reports
gitdirs=). A stale or swapped plantherefore executed its full gated content while the banner named a narrower tier —
e.g. a
--tier builddry-run plan applied with--tier cachesremoved bothbin/and
.pytest_cache/while printingTier: caches. Bounded defect: every removedpath was still enumerated and confirmation-gated at plan creation, so nothing
un-gated is ever removed — the flaw is scope misrepresentation at apply time.
Fix
Per the triage decision on #1081 (validate, do not drop the flag),
--applynowpre-scans the whole plan before the banner and before touching any disk and
refuses it atomically (usage error, exit 2, nothing removed, no apply banner) when a
record the requested
--tierdoes not authorize is present:REPOtokencaches--tier cachesREPOtokenbuild--tier build,--tier allGITDIR--tier git,--tier allThis rejects a
build-class REPO record under--tier cachesand gates theGITDIRarm on a git-bearing tier, exactly as the triage resolved. Atomic refusal(vs. per-record rejection) means the apply banner is never printed for a mismatched
plan, so the misrepresentation is structurally impossible rather than merely caught
after some records already ran. A malformed/unrecognized token is still handled as
before by the apply loop's per-record fail-closed guard (exit 1) — a distinct error
class (structural corruption) from a well-formed plan built for the wrong tier.
Authorization uses a dedicated
tier_repo_tokenhelper (empty for the git tier) —deliberately not the existing
manifest_child_token, which returnsbuildforgit and would wrongly authorize a build REPO record under
--tier git.Header /
usage/clean-batch.mdexit-taxonomy and gated-set docs updated to match.Verification
All commands run in the worktree on this branch.
Full test suite (57 assertions, incl. 3 new tier-authorization tests) + shellcheck:
Exact issue repro (a
--tier builddry-run plan applied with--tier caches) —both
bin/and.pytest_cache/survive:Version:
plugins/repo-hygienebumped 0.7.1 → 0.7.2 (patch bugfix) with a matchingCHANGELOG
[0.7.2]Fixedentry.mainpublished its own doc-only0.7.1whilethis branch was open, so this fix takes
0.7.2— otherwise consumers already on thepublished
0.7.1would never see it.main's[0.7.1]entry is preserved verbatim.Related
this issue is the deferred Codex P2 finding from that review.
validation ("reject
build-class REPO records under--tier caches; gate theGITDIRarm on a git-bearing tier — veto before merge"); this PR implements thatdirection exactly.
Closes #1081
Work-class: C3 (bug-fix-shaped) — attended triage 2026-07-23, operator-ratified. 🤖