origin-git-cleanup
Wrapping up a whole chunk of work? If the repo uses
docs/governance, prefer the origin-close-session skill instead of calling origin-git-cleanup directly — it runs origin-doc-update first so documentation and git are closed out together and the doc edits land in the same commit. Use origin-git-cleanup directly for git-only housekeeping (no docs to update).
Bring a repository to a clean, ready-for-next-task state after a unit of work is done. Target end state:
- current branch is main (or the repo's integration branch),
- working tree is clean,
- main == origin/main,
- branches and worktrees that were only needed for the finished work are gone,
- everything still present is there on purpose.
Why this needs judgment, not a script
The dangerous failure mode is acting mechanically. A repo often has parallel work in flight — other branches, other worktrees, someone else's open PR. If you blindly delete branches or sweep stray edits into a commit, you destroy work that wasn't yours to touch. So the whole skill is built around one shape:
Survey (read-only) → Classify every branch/worktree → Propose a plan → Execute integration → Verify root/main cleanliness.
When anything is ambiguous, stop and ask rather than guess. Leaving something alone is always safe; deleting the wrong thing is not.
Cardinal rules
- Read before you write. Never mutate state before you've surveyed it.
- Propose before you destroy. Present the full plan before execution.
Commit, push, and a green PR's merge are autonomous under the
cdmerge policy; underhuman-gatedthe merge itself needs approval (see Inputs). Get approval before any forced operation or other destructive cleanup. - Deletion authority follows provenance. Removing a branch this run itself created and has just merged is part of that merge, not a separate destruction, and needs no extra approval. Carry it out as its own step after the merge rather than as a flag on it, because a merge that also deletes is the form a permission classifier most often refuses, and a refusal there strands an unattended run one step short of done. If a merge is refused even without the flag, leave the PR open and surface it — an open PR with green checks is a legal end state, and it costs one message instead of the run. Deleting anything this run did not create — a pre-existing branch, someone else's worktree, a remote ref you did not push — still requires approval. What the rule protects against is losing work you never saw, which cannot describe work you just wrote, merged, and verified.
- Don't guess about parallel work. If a branch or worktree might belong to other active work, leave it and say so.
- Prefer the reversible. Use
git branch -d(never-D); nevergit push --force,git reset --hard, orgit clean -fdwithout an explicit instruction from the user. - Follow repo conventions. Use the
ghCLI for all GitHub operations. Match the repository's existing commit-message style. - Classify every leftover explicitly. Nothing should remain as "probably
active." Each branch/worktree must end as
integrate,delete,keep, orinvestigate, with a short reason. - Root worktree must finish clean on main. Cleanup is not complete if the repository root is still on a feature branch, detached HEAD, dirty, or behind origin.
- Patch-equivalence beats branch ancestry. Squash merges and cherry-picks
often make
git branch --mergedlie by omission. Use patch/stat inspection before deciding a branch is unmerged work. - Write context-rich commit messages. A good commit message explains not just what changed but why — the problem that prompted the work, the decision made, and any non-obvious constraints. Future readers (including AI agents doing code archaeology) should be able to reconstruct the reasoning without needing to ask. A one-line summary is rarely enough. When proposing the commit message in Stage 3, draft a multi-line body that captures: what was broken or missing, what was investigated, what approach was chosen, and what was confirmed to work.
Inputs
repository — the path being cleaned up. Every script takes it; do not infer it from the current directory (see Stage 1).
merge policy —
cd(default) orhuman-gated. A caller that reserved the merge for a human must say so here, and this skill must ask when it was not told. Nothing else in these instructions can tell the difference: the cardinal rules call a green PR's merge autonomous, Stage 4 merges unconditionally, and bothsurvey.shand Stage 5 treat an unmerged PR as an unfinished job and push toward the merge. So a reservation carried only in the caller's prose gets merged under this skill's own authority, and the report reads as a normal landing — there is no artifact anywhere saying a gate was passed over.Under
human-gated, the finished state is different and must not be chased past: committed, pushed, PR open, branch present, tree clean. Render the merge step under Requires confirmation in Stage 3, do not merge, do not delete the branch the open PR needs, and readroot_ready: noas expected rather than as something to fix.
Stage 1 — Survey (read-only)
Run the bundled snapshot script naming the repository you are cleaning up. It only reads, never writes:
bash ~/.agents/skills/origin-git-cleanup/scripts/survey.sh --repo <repo path>
--repo is not optional bookkeeping. Without it the script surveys the current
directory, and when this skill is reached through an orchestrator invoked
against a repository — origin-ws-loop, origin-close-session — the current
directory is a different repository. That survey succeeds, reports the wrong
repository's branches and worktrees, and those become the input to Stage 3's
deletion plan. Check the toplevel: line in the output against the repository
you meant before reading anything else.
It reports: current branch, remote, root cleanliness, every worktree's branch / HEAD / dirty status, upstream ahead/behind, stashes, local and remote branches, branches merged vs not merged into main, recent history, and open PRs.
Run it every time you reach this stage, and say so when you did not. Reached
once per iteration of a longer run, this survey is cheap next to what depends on
it: Stage 2's classification and Stage 3's deletion plan have no other source of
truth. Substituting a few ad-hoc git commands leaves those decisions resting on
a partial picture, and nothing in the resulting report distinguishes that from a
full survey unless the report says which iterations skipped it.
If the script isn't available, gather the same picture manually with
git status, git branch -vv, git branch -a, git worktree list --porcelain,
git stash list, git log --graph --oneline -15,
git branch --merged main / --no-merged main, and gh pr status.
If the repo has docs/issues/ (origin-doc-update in use), also run this — it
cross-checks origin-doc-update's issue branch fields against real git state so
Stage 2 doesn't have to guess by name alone:
python3 ~/.agents/skills/origin-git-cleanup/scripts/check_active_issue_branches.py <repo path>
It reports which active issues' recorded branch is missing (a signal that
resuming the issue needs a fresh branch, or that the branch was cleaned up
without archiving the issue), and which ISSUE-*-shaped branches have no
active issue referencing them (orphan candidates). It no-ops cleanly if
docs/issues/ doesn't exist or the repo isn't using origin-doc-update.
Stage 2 — Classify and decide
Work through each branch/worktree and assign exactly one disposition:
| Disposition | Meaning |
|---|---|
integrate |
Contains useful work that should be committed/merged/cherry-picked |
delete |
Confirmed obsolete, duplicate, generated residue, or superseded |
keep |
Intentional parallel work; leave it and record why |
investigate |
Not enough evidence; inspect diffs/PRs before any mutation |
Do not leave a branch/worktree unclassified. Note the conclusion and evidence so it can be presented in Stage 3.
Uncommitted / untracked changes
Read the actual diff (git diff, git diff --staged, and look at untracked
files). Also check the modification dates of untracked files (ls -la or
stat). Files with old dates (weeks or months ago) may be forgotten artifacts —
flag them separately so the user can decide whether to commit, archive, or delete
them rather than silently folding them into the current task's commit.
Then decide:
- Part of the finished task → propose committing them. Draft a message that includes the subject line (repo style) and a multi-line body explaining the background: what was broken or missing, what was investigated, what approach was chosen, and what was verified. This context helps future readers — human or AI — reconstruct the reasoning without having to dig through Slack or PR comments.
- Unrelated or half-done → propose
git stash(with a label) or simply leaving them in place. Do not fold unrelated edits into the task's commit. - Generated junk / secrets / large artifacts → flag it; don't commit.
When intent is unclear, ask which bucket the changes fall into.
Current branch — the usual cleanup target, and the easiest to miss
You are almost always running this skill while checked out on the very branch you just finished. That branch is normally the main thing to remove, but two traps make cleanup silently leave it behind — locally and on the remote:
- A squash-merged branch looks unmerged locally. After a squash (or
cherry-pick), the branch's commits are not ancestors of your local main, so
git branch --merged mainomits it and the survey may list it under "not merged / likely active work." Don't trust that label for the branch you're on: confirm via its PR state (gh pr view) or patch-equivalence (git diff main..HEADis empty → already in main). If it's merged, it's adelete, notkeep. - You cannot delete the branch you're standing on.
git branch -d <current>fails while it's checked out. The branch must be deleted after the root worktree is switched back to main (Stage 4 enforces this order).
Classify it explicitly:
- Merged (by ancestry, squash, or cherry-pick)? →
delete. Plan to switch to main first, then delete the local branch and its remote/tracking ref. - Has unpushed commits / no PR yet? →
integrate(merge it first, then delete). - Is it main itself? → there's no feature branch to merge; focus on syncing and pruning.
Patch-equivalence and obsolete branch checks
Use these when a branch is not ancestry-merged but looks already handled:
Compare patch identity for a suspected squash/duplicate commit:
git show <branch-commit> -- <paths> | git patch-id --stable git show <main-commit> -- <paths> | git patch-id --stableCompare the actual payload before deleting:
git show --stat --name-status <branch> git diff --stat main..<branch> git log --oneline --left-right main...<branch>If the useful work was cherry-picked or manually reimplemented on main, mark the old branch
delete (superseded by <main-sha>). This is one of the few valid cases forgit branch -D, but only after explicit user approval.
Merge strategy (auto-detect)
- GitHub remote present →
ghPR + squash merge. This gives a numbered, reviewable record, lets CI gate the merge, keeps main as a clean one-commit- per-feature history, and makes reverts trivial. - No remote (local-only repo) → local fast-forward merge into main.
- Cross-check against recent history: if the repo clearly uses merge commits rather than squash, follow what the repo already does.
Branch deletion candidates
A local branch is a candidate only if all hold:
- it appears under
git branch --merged main, and - it is not a protected branch (
main/master/develop/release/*), and - it doesn't look like active parallel work (see below).
Delete with git branch -d (safe; refuses if not merged). Never -D.
Note the common case: after a squash merge, the feature branch is merged on
the remote but git's local --merged check (against your local HEAD) may still
warn it's "not fully merged." Once main is updated from origin, deleting it is
safe — gh pr merge --delete-branch handles the remote side, and git branch -d
works locally after main is synced. If -d still refuses for a branch you've
confirmed is squash-merged via its PR, surface that to the user and let them
decide; don't reach for -D on your own.
Worktrees
Remove a worktree only if it was for a branch you just merged/deleted and it is clean. Never remove a worktree with uncommitted changes — report it instead.
If the approved target is the worktree hosting the current session, do not run
the removal while the command process is still inside that path. Select a safe
worktree that will remain (normally the repository root on main) and run the
directory change and removal in the same shell invocation, for example:
cd <safe-worktree> && git worktree remove <target-worktree>
A separate cd tool call does not persist into the next command. Afterward,
run git worktree list from the safe worktree and confirm the target is gone.
This cwd rule does not relax the existing ownership, cleanliness, parallel-use,
or deletion-approval checks.
Special cases:
- A repository root worktree cannot be removed. If it is on a doomed branch, first
move another worktree off
mainif needed, then checkoutmainin the root. - A detached clean worktree may be removed when it points to an obsolete temporary deployment or inspection commit and has no uncommitted files.
- Dirty worktrees require a decision: commit, stash, keep, or discard. Discard
(
git checkout -f,rm,git worktree remove --force) only with explicit user approval and after listing the files.
Protecting parallel work
A branch matching an active issue's branch field (per
check_active_issue_branches.py's OK list) is keep unconditionally — this
is a decisive signal, not a guess, and overrides any name-based heuristic
below. Reason: "active issue <id>".
Otherwise, leave a branch/worktree alone if any of these are true:
- its name is unrelated to the finished task,
- it has recent commits you didn't make as part of this work,
- it has an open PR,
- it's checked out in another worktree,
- you're simply not sure.
An orphan-candidate branch from the check script (looks issue-shaped, no
active issue references it) is not an automatic delete — treat it as
investigate input alongside the usual patch-equivalence checks.
Surface what you're leaving and why, so the user can override if they want.
Deletion protocol for destructive cleanup
git branch -D, git worktree remove --force, rm of untracked files, and
remote branch deletion are allowed only when all are true:
- The target has been inspected (
status,diff/show, and PR/remote state where relevant). - The target is classified as
delete. - The user has approved deletion or explicitly instructed deletion.
- The final report names what was deleted and why.
Never use git reset --hard or git clean -fd as a shortcut for this protocol.
Stage 3 — Propose the plan and isolate destructive cleanup
Present a single structured summary. Execute its commit, push, PR creation, and green PR merge steps autonomously. If it includes branch/worktree/ref deletion, separate those steps and request one confirmation before performing only those actions:
## origin-git-cleanup plan
Will do autonomously:
- Commit <files> on <branch> — "<message>" (reason)
- Push <branch> and merge via <PR+squash | local ff> into main
- Switch to main and sync with origin/main
Requires confirmation before deletion:
- Delete merged branch: <name> (merged via PR #N)
- Remove worktree: <path> (clean, branch merged)
- Prune stale remote-tracking refs
Will leave alone:
- Branch <name> — keep: unrelated active work, has open PR #M
- Worktree <path> — investigate: has uncommitted changes
- Stash <ref> — unrelated
If the user wants changes, adjust and re-present. Don't execute anything destructive until they approve.
Stage 4 — Execute (safe order, stop on first error)
- Commit or stash uncommitted changes per the decision.
- Push the current branch if it has unpushed commits.
- Merge into main:
- GitHub:
gh pr create …thengh pr merge <N> --squash, retiring the remote ref in step 10 rather than with a flag on the merge. - Local:
git checkout main && git merge --ff-only <branch>.
- GitHub:
- Sync main:
git checkout main && git pull --ff-only. - Return the repository root worktree to main if it is not already there. Do this before any branch deletion — you cannot delete the branch you're on, so the just-finished current branch stays undeletable until you've switched away.
- Delete merged local branches:
git branch -d <name>. This now includes the branch you started on, once you've switched to main. If-drefuses a branch you've confirmed squash-merged via its PR, surface it (don't reach for-D). The remote counterpart is retired in step 10, since step 3 merges without a deletion flag. - Delete superseded branches with
git branch -D <name>only when approved. - Remove approved clean worktrees. For a target that contains the current
session, first choose a surviving safe worktree and run
cd <safe-worktree> && git worktree remove <target-worktree>in one shell invocation; otherwise usegit worktree remove <path>from a safe worktree. Verify immediately withgit worktree list. - Remove approved dirty/superseded worktrees with
git worktree remove --forceonly after file-level inspection and approval. - Retire remote branches with
git push origin --delete <name>. This covers both the branch this run created and merged in step 3, which needs no separate approval, and any other remote branch, which does. - Prune stale remote refs:
git remote prune origin.
If a step errors, stop and report — don't push past failures.
If a deleted/merged branch matched an active issue's branch field, add a
line to the final report suggesting the issue be archived — don't archive it
yourself; whether specs/guides/index updates are complete is origin-doc-update's
call, not this skill's:
- Branch <name> deleted (issue <id>) — if the work is complete, archive it:
python3 ~/.agents/skills/origin-doc-update/scripts/archive_issue.py <id>
Stage 5 — Verify
Confirm and report the end state:
git statusis clean,- repository root is on
main(or the integration branch), git rev-parse main==git rev-parse origin/main(or report ahead/behind),git worktree listcontains only intentional worktrees,git branch -vvcontains only intentional branches,- every remaining branch/worktree is listed with its disposition and reason.
Under the cd merge policy you should finish on main, clean, and synced —
ready to branch off for the next task.
Under human-gated the success shape is different, and it is a success:
committed, pushed, PR open, branch present, tree clean. Report it as finished.
Do not merge to reach "on main", and do not delete the branch the open PR needs —
those are the two ways this verification step turns a reserved decision into a
completed one, and the report would not say that anything was overridden.
Quick reference: command safety
| Safe (reversible) | Needs care (in plan) | Forbidden without explicit ask |
|---|---|---|
git status, git branch, git log, git diff, survey.sh, check_active_issue_branches.py |
git commit, git push, gh pr merge, git branch -d, git worktree remove, git remote prune |
git branch -D, git push --force, git reset --hard, git clean -fd |