repo-cleanup-loop
A configuration of loop-controller. That skill supplies the loop
machinery — primitive selection, the full guardrail stack, state
externalization. This skill supplies the three things specific to "keep the
repo state intentional": the per-class recipe (it delegates the
post-merge-branch case to git-post-merge-cleanup, it does not re-implement
it), the mechanical proof (a classified inventory where nothing is
"safely removable" until evidence flips it), and the recover-before-delete
HITL boundary that makes deletion safe. Read loop-controller for the
guardrails; they're inherited, not repeated here.
Why disable-model-invocation: this loop deletes branches, prunes refs,
and removes worktrees on its own, on a clock — irreversible acts. It is
user-driven: you want to type /loop it weekly (or run /repo-cleanup-loop
once), not have Claude silently start pruning because it noticed debris.
The 5-part contract
| Part |
This loop |
| trigger |
a weekly cadence (/loop or /schedule) over the whole repo, or an explicit /repo-cleanup-loop |
| action |
ONE pass: scan repo state (branches + PRs + commits + worktrees) → classify each item → recover anything valuable → remove one class of safely-removable items with evidence → re-scan |
| proof |
the repo state is intentional — every branch / PR / worktree is current OR owned OR safely-removed-with-evidence — default-FAIL: each inventory item starts "reviewed": false and only flips on classification + evidence |
| memory |
the classified inventory JSON (per-item: kind, name, class, evidence, "reviewed", "recovered"), a recovery log (what was salvaged + where), and git history / reflog as the durable undo trail |
| stop |
every item reviewed and intentional (proof) OR iteration/pass cap OR no-progress for 3 passes (same item unresolved) OR budget cap OR an HITL checkpoint on an ambiguous item |
The proof: a classified inventory, default-FAIL
"Tidy" is not "I deleted some merged branches." It is every item in the repo's
git state accounted for: each branch, open PR, dangling commit trail, and
worktree is classified current (active work), owned (someone's in-flight
work — leave it), or safely-removable with recorded evidence that it was
merged or its work recovered. Assume every item is unreviewed until the scan
classifies it — that's the default-FAIL stance, and it's why the inventory JSON
stores "reviewed": false per item rather than a prose summary. A model is far
less likely to quietly flip a JSON false than to soften a sentence.
Name the artifact explicitly: the inventory JSON written each pass (one record
per branch / PR / worktree). The pass is only done when no record reads
"reviewed": false and no record is safely-removable without an evidence
field. The classify / recover / remove algorithm and the exact evidence rules
are in references/cleanup-algorithm.md.
Step 1 — One pass = scan → classify → recover → remove one class → re-scan
The per-pass work is a single sweep, not a free-for-all:
- Scan the whole repo's git state — local + remote branches, open PRs (via
gh), worktrees, and stale/dangling refs (git worktree list,
git branch -vv, git for-each-ref, gh pr list).
- Classify each item into the inventory (default-FAIL). Evidence rules and
the bucket definitions live in
references/cleanup-algorithm.md.
- Recover before you delete. Any item that looks removable but holds work
not on the default branch goes through the recovery step first — see the HITL
section. This is the load-bearing safety act.
- Remove exactly one class of safely-removable items with evidence (e.g.
"fully-merged local branches" this pass, "gone remote-tracking refs" next).
One class per pass keeps the signal about what each removal changed.
- Re-scan and re-classify. The proof is about the whole state, re-checked
— never trust last pass's inventory.
Delegate the post-merge-branch case — do not re-implement it. For the
fully-merged-local-branch, gone-remote-tracking-ref, and safe-worktree classes,
invoke git-post-merge-cleanup (its --dry-run for the scan, then its
confirmed execute). That skill already owns the branch/worktree classification
and the destructive-command ordering. This loop adds the discipline around it:
the default-FAIL whole-repo inventory, the recover-before-delete gate, the
one-class-per-pass cadence, and the weekly schedule. The exact delegation
boundary — what this loop hands off versus what it keeps — is in
references/cleanup-algorithm.md.
Step 2 — Schedule it with /loop or /schedule (the watcher, not /goal)
Per loop-controller Step 1: this is a watch/poll job on a weekly cadence —
you keep the repo's state intentional as work accrues, you do not push it to a
single finish line — so the primitive is /loop (session-scoped) or
/schedule (a cloud cron that survives laptop-off — the better fit for a
weekly cadence), not /goal. /loop's session-scope / ~3-day-expiry /
no-catch-up mechanics live in loop-controller's references/primitives.md.
How this differs from its neighbors
The one real overlap is git-post-merge-cleanup, and the boundary is
deliberate:
git-post-merge-cleanup is a focused one-shot tidy: after a batch of
PRs land, scan → classify → present a plan → act on confirmation, for three
specific classes (merged local branches, gone remote-tracking refs, stale
worktrees). It runs once, when you ask, and it is the expert on the branch
and worktree classification.
repo-cleanup-loop is the broader, recurring, evidence-gated weekly
loop over the whole repo — branches and open PRs and commit trails and
worktrees — that recovers valuable work before deleting, removes one class
per pass, and re-scans until the whole inventory is intentional.
It is not a thin scheduled wrapper: the substance is the loop discipline, not
the branch logic it borrows — the default-FAIL whole-repo inventory, the
recover-before-delete step, one-class-per-pass, and the weekly schedule. When a
pass hits the merged-branch / gone-ref / safe-worktree classes it delegates
to git-post-merge-cleanup rather than re-implementing them. What it owns that
the one-shot does not: the recurring cadence, the PR + commit-trail scan, and the
recover-first gate.
HITL is load-bearing for this loop
Deletion is the irreversible act, and recovering valuable work before deleting
is the load-bearing safety step. This loop runs only inside the reversible
boundary unattended; anything ambiguous is a human checkpoint — never
autonomous:
- Never delete a branch / worktree / ref without evidence it is safely
removable — merged into the default branch, or its work demonstrably
recovered. No evidence = not removable, full stop.
- Recover before delete. An item that holds commits not on the default
branch is valuable work, not debris. Salvage it first (a
recovery/<name>
ref or tag, the commits cherry-picked or noted, the diff archived) and log
where it went — then it is eligible for removal. Recovery happens before the
delete, every time.
- Never force-delete unmerged work. Lowercase
git branch -d (refuses
unmerged) before uppercase -D; -D only after merge is confirmed or the
human explicitly asks. No git push --force, no --force worktree removal of
genuinely-unmerged work, unattended.
- Ambiguous item = pause. Squash-merged-but-shows-unmerged, a stale open PR
with unpushed local work, a worktree with uncommitted diffs that don't match
the default branch, a long-lived
develop/staging/production branch —
surface it, do not act. The "needs attention" bucket is the signal.
The full AFK-safe vs pause table and the recovery recipe are in
references/cleanup-algorithm.md.
Guardrails specific to this loop
Inherits the full stack from loop-controller → references/safety.md. The caps
this loop sets:
- Pass / iteration cap — default ~10 passes per run (read from
.claude/profile.yaml when present). One class removed per pass; hitting the
cap is a stop-and-report, not a license to bulk-delete the rest.
- No-progress detection — if the same item stays
"reviewed": false
across 3 consecutive passes (can't be classified, or can't be safely
removed within the boundary), stop and surface it. Three passes on one item
means it needs a human, not a fourth scan.
- Budget cap — a weekly scheduled sweep adds up over months; enforce a
token/cost ceiling that terminates the run, not just warns.
- Never cheat the proof. Don't mark an item
safely-removable without an
evidence field, don't git branch -D to clear an unmerged item from the
count, and don't skip the recovery step to "tidy faster." A clean inventory
that came from force-deleting unrecovered work is a finding, not a win.
Choosing the driver primitive
/schedule (preferred for weekly) or /loop is the scheduler. The per-pass
exit — this pass classified the inventory, recovered what was valuable, and
removed one class with evidence — is provable from git/gh output and the
inventory JSON, so a pass runs unattended within the HITL boundary above. The
loop as a whole has no /goal finish line; it watches and tidies on a
cadence until you stop it or a stop condition fires.
Reference files
references/cleanup-algorithm.md — the scan → classify → recover → remove
algorithm, the per-class evidence rules and inventory-JSON shape, the
recover-before-delete recipe, the AFK-safe vs pause table, and the exact
delegation boundary to git-post-merge-cleanup (what to hand off, what to keep).
1---2name: repo-cleanup-loop3description: Keep a repo's git state intentional on a weekly cadence: scan every branch, PR, commit trail, and worktree, classify each as current / owned / safely removable-with-evidence, RECOVER any valuable work before deleting, then remove exactly one class of stale items with evidence — looping until every item is accounted for, with an HITL checkpoint on anything ambiguous and never a force-delete of unmerged work. The broad recurring evidence-gated loop that delegates the post-merge-branch case to git-post-merge-cleanup. Use when you want a repo kept tidy on a schedule, stale branches and worktrees pruned safely after sprints, or a weekly hygiene sweep that recovers before it deletes. Trigger on "clean up the repo weekly", "weekly repo hygiene", "prune stale branches and worktrees on a schedule", "tidy git state every week", "scheduled repo cleanup", "sweep stale PRs and worktrees". A configuration of loop-controller.4---56# repo-cleanup-loop78> **A configuration of [`loop-controller`].** That skill supplies the loop9> machinery — primitive selection, the full guardrail stack, state10> externalization. This skill supplies the three things specific to "keep the11> repo state intentional": the **per-class recipe** (it delegates the12> post-merge-branch case to [`git-post-merge-cleanup`], it does not re-implement13> it), the **mechanical proof** (a classified inventory where nothing is14> "safely removable" until evidence flips it), and the **recover-before-delete15> HITL boundary** that makes deletion safe. Read `loop-controller` for the16> guardrails; they're inherited, not repeated here.17>18> **Why `disable-model-invocation`:** this loop deletes branches, prunes refs,19> and removes worktrees on its own, on a clock — irreversible acts. It is20> user-driven: you want to *type* `/loop` it weekly (or run `/repo-cleanup-loop`21> once), not have Claude silently start pruning because it noticed debris.2223## The 5-part contract2425| Part | This loop |26|---|---|27| **trigger** | a weekly cadence (`/loop` or `/schedule`) over the whole repo, or an explicit `/repo-cleanup-loop` |28| **action** | ONE pass: scan repo state (branches + PRs + commits + worktrees) → classify each item → **recover** anything valuable → remove **one class** of safely-removable items **with evidence** → re-scan |29| **proof** | the repo state is intentional — **every** branch / PR / worktree is `current` OR `owned` OR `safely-removed-with-evidence` — default-FAIL: each inventory item starts `"reviewed": false` and only flips on classification + evidence |30| **memory** | the classified inventory JSON (per-item: kind, name, class, evidence, `"reviewed"`, `"recovered"`), a recovery log (what was salvaged + where), and git history / reflog as the durable undo trail |31| **stop** | every item `reviewed` and intentional (proof) **OR** iteration/pass cap **OR** no-progress for 3 passes (same item unresolved) **OR** budget cap **OR** an HITL checkpoint on an ambiguous item |3233## The proof: a classified inventory, default-FAIL3435"Tidy" is not "I deleted some merged branches." It is **every item in the repo's36git state accounted for**: each branch, open PR, dangling commit trail, and37worktree is classified `current` (active work), `owned` (someone's in-flight38work — leave it), or `safely-removable` **with recorded evidence** that it was39merged or its work recovered. Assume every item is **unreviewed** until the scan40classifies it — that's the default-FAIL stance, and it's why the inventory JSON41stores `"reviewed": false` per item rather than a prose summary. A model is far42less likely to quietly flip a JSON `false` than to soften a sentence.4344Name the artifact explicitly: the inventory JSON written each pass (one record45per branch / PR / worktree). The pass is only done when no record reads46`"reviewed": false` and no record is `safely-removable` without an `evidence`47field. The classify / recover / remove algorithm and the exact evidence rules48are in `references/cleanup-algorithm.md`.4950## Step 1 — One pass = scan → classify → recover → remove one class → re-scan5152The per-pass work is a single sweep, not a free-for-all:53541. **Scan** the whole repo's git state — local + remote branches, open PRs (via55 `gh`), worktrees, and stale/dangling refs (`git worktree list`,56 `git branch -vv`, `git for-each-ref`, `gh pr list`).572. **Classify** each item into the inventory (default-FAIL). Evidence rules and58 the bucket definitions live in `references/cleanup-algorithm.md`.593. **Recover before you delete.** Any item that *looks* removable but holds work60 not on the default branch goes through the recovery step first — see the HITL61 section. This is the load-bearing safety act.624. **Remove exactly one class** of safely-removable items with evidence (e.g.63 "fully-merged local branches" this pass, "gone remote-tracking refs" next).64 One class per pass keeps the signal about what each removal changed.655. **Re-scan** and re-classify. The proof is about the *whole* state, re-checked66 — never trust last pass's inventory.6768**Delegate the post-merge-branch case — do not re-implement it.** For the69fully-merged-local-branch, gone-remote-tracking-ref, and safe-worktree classes,70invoke [`git-post-merge-cleanup`] (its `--dry-run` for the scan, then its71confirmed execute). That skill already owns the branch/worktree classification72and the destructive-command ordering. This loop adds the *discipline around* it:73the default-FAIL whole-repo inventory, the recover-before-delete gate, the74one-class-per-pass cadence, and the weekly schedule. The exact delegation75boundary — what this loop hands off versus what it keeps — is in76`references/cleanup-algorithm.md`.7778## Step 2 — Schedule it with /loop or /schedule (the watcher, not /goal)7980Per `loop-controller` Step 1: this is a **watch/poll** job on a weekly cadence —81you keep the repo's state intentional as work accrues, you do not push it to a82single finish line — so the primitive is **`/loop`** (session-scoped) or83**`/schedule`** (a cloud cron that survives laptop-off — the better fit for a84*weekly* cadence), **not** `/goal`. `/loop`'s session-scope / ~3-day-expiry /85no-catch-up mechanics live in `loop-controller`'s `references/primitives.md`.8687## How this differs from its neighbors8889The one real overlap is **[`git-post-merge-cleanup`]**, and the boundary is90deliberate:9192- **`git-post-merge-cleanup`** is a **focused one-shot tidy**: after a batch of93 PRs land, scan → classify → present a plan → act on confirmation, for three94 specific classes (merged local branches, gone remote-tracking refs, stale95 worktrees). It runs once, when you ask, and it is the *expert* on the branch96 and worktree classification.97- **`repo-cleanup-loop`** is the **broader, recurring, evidence-gated weekly98 loop over the whole repo** — branches *and* open PRs *and* commit trails *and*99 worktrees — that **recovers valuable work before deleting**, removes one class100 per pass, and re-scans until the whole inventory is intentional.101102It is **not** a thin scheduled wrapper: the substance is the loop discipline, not103the branch logic it borrows — the default-FAIL whole-repo inventory, the104recover-before-delete step, one-class-per-pass, and the weekly schedule. When a105pass hits the merged-branch / gone-ref / safe-worktree classes it **delegates**106to `git-post-merge-cleanup` rather than re-implementing them. What it owns that107the one-shot does not: the recurring cadence, the PR + commit-trail scan, and the108recover-first gate.109110## HITL is load-bearing for this loop111112Deletion is the irreversible act, and **recovering valuable work before deleting113is the load-bearing safety step.** This loop runs only inside the reversible114boundary unattended; anything ambiguous is a **human checkpoint — never115autonomous**:116117- **Never delete a branch / worktree / ref without evidence it is safely118 removable** — merged into the default branch, or its work demonstrably119 recovered. No evidence = not removable, full stop.120- **Recover before delete.** An item that holds commits not on the default121 branch is *valuable work*, not debris. Salvage it first (a `recovery/<name>`122 ref or tag, the commits cherry-picked or noted, the diff archived) and log123 where it went — *then* it is eligible for removal. Recovery happens before the124 delete, every time.125- **Never force-delete unmerged work.** Lowercase `git branch -d` (refuses126 unmerged) before uppercase `-D`; `-D` only after merge is confirmed or the127 human explicitly asks. No `git push --force`, no `--force` worktree removal of128 genuinely-unmerged work, unattended.129- **Ambiguous item = pause.** Squash-merged-but-shows-unmerged, a stale open PR130 with unpushed local work, a worktree with uncommitted diffs that don't match131 the default branch, a long-lived `develop`/`staging`/`production` branch —132 surface it, do not act. The "needs attention" bucket is the signal.133134The full AFK-safe vs pause table and the recovery recipe are in135`references/cleanup-algorithm.md`.136137## Guardrails specific to this loop138139Inherits the full stack from `loop-controller` → `references/safety.md`. The caps140this loop sets:141142- **Pass / iteration cap** — default ~10 passes per run (read from143 `.claude/profile.yaml` when present). One class removed per pass; hitting the144 cap is a *stop-and-report*, not a license to bulk-delete the rest.145- **No-progress detection** — if the **same item** stays `"reviewed": false`146 across **3 consecutive passes** (can't be classified, or can't be safely147 removed within the boundary), stop and surface it. Three passes on one item148 means it needs a human, not a fourth scan.149- **Budget cap** — a weekly scheduled sweep adds up over months; enforce a150 token/cost ceiling that *terminates* the run, not just warns.151- **Never cheat the proof.** Don't mark an item `safely-removable` without an152 `evidence` field, don't `git branch -D` to clear an unmerged item from the153 count, and don't skip the recovery step to "tidy faster." A clean inventory154 that came from force-deleting unrecovered work is a *finding*, not a win.155156## Choosing the driver primitive157158`/schedule` (preferred for weekly) or `/loop` is the scheduler. The *per-pass*159exit — this pass classified the inventory, recovered what was valuable, and160removed one class with evidence — is provable from `git`/`gh` output and the161inventory JSON, so a pass runs unattended **within the HITL boundary above**. The162loop as a whole has **no `/goal` finish line**; it watches and tidies on a163cadence until you stop it or a stop condition fires.164165## Reference files166167- `references/cleanup-algorithm.md` — the scan → classify → recover → remove168 algorithm, the per-class evidence rules and inventory-JSON shape, the169 recover-before-delete recipe, the AFK-safe vs pause table, and the exact170 delegation boundary to `git-post-merge-cleanup` (what to hand off, what to keep).171172[`loop-controller`]: ../loop-controller/SKILL.md173[`git-post-merge-cleanup`]: ../../git/git-post-merge-cleanup/SKILL.md