Wrap-up / post-merge local Git hygiene:Return to default branch → pull latest → only clean worktrees and local branches confirmed to have no unique work。
Division of labor with existing skills:
| Skill | What it does |
|---|---|
| git-hygiene (this skill) | Return to main, ff-only pull, clean worktrees/branches based on "whether there are still unique commits relative to origin/main" |
git-worktree(compound-engineering) |
Create / switch / interactively clean .worktrees/ convention parallel worktrees under |
blocklet-branch |
Per blocklet repo conventionopeniteration branch |
This skill does notmerge PRs for you, does not force-push, does not git clean -fdx, does not lose dirty changes.
Triggers
- "switch to main / pull latest / clean up useless worktrees and branches"
- "confirm there is no unmerged work before deleting"
- Local wrap-up after a PR has been merged
- Main workspace reset after multi-agent parallel work ends
- "Are these KEEP branches supposed to have PRs filed / help me see when the latest changes were / check carefully that if not merged, file PRs for all of them" → triggers the §4.5-4.7 deep-dive process (by default the §3-6 main process does not do this layer, it's too expensive; only investigate each one when the user explicitly requests it)
Default assumptions
- Remote name:
origin - Default branch: prefer
origin/HEAD→ otherwisemain→ otherwisemaster - Deletion gate = content is already on
origin/<default>on, not "I remember I merged it"
Hard rules
- Main workspace dirty → stop. When there are uncommitted changes, do not
checkout, do not delete branches. First have the user commit / stash / explicitly discard. - Only
git pull --ff-only。 rejects non-fast-forward; if there is divergence, report it, do not rebase/merge to fix it on your own. - Do not delete branches that still have unique commits.
git merge-base --is-ancestor <branch> origin/<default>is false → KEEP, unless going through squash verification (see below). - Do not delete dirty / locked worktrees.
- Do not delete the currently checked-out branch(switch to default first).
- The main worktree never
worktree remove。 - Remote
origin/<branch>being gone ≠ locally deletable. Gone only means it's gone from the remote; the local tip may still have unmerged commits. - One approval only covers this one time. When the user says "continue cleaning", still handle the KEEP list category by category, do not default to
branch -Dall unmerged.
Process
0. Scope
- Default: current repo (
git rev-parse --show-toplevel) - User names multiple repos (e.g. site + arc):run the full process separately for each repo, write reports separately
Prefer running the deterministic script (inventory; optional apply). The script is next to the skill:
# From agentloop plugin install (team):
ROOT="${AGENTLOOP_ROOT:-$HOME/.claude/plugins/marketplaces/arcblock-agent-skills/plugins/agentloop}"
# Or when developing in arc SoT:
# ROOT="$(git rev-parse --show-toplevel)/.claude/plugins/agentloop"
bash "$ROOT/skills/git-hygiene/scripts/git-hygiene.sh" inventory
# 审查报告后:
bash "$ROOT/skills/git-hygiene/scripts/git-hygiene.sh" apply --yes
# 多 repo:
bash "$ROOT/skills/git-hygiene/scripts/git-hygiene.sh" inventory /path/to/other-repo
The agent must read the script output before deciding;apply only handles the SAFE category. SQUASH_CANDIDATE / KEEP are handled by the agent according to the rules below.
1. Inventory (read-only)
Collect and display:
| Item | Command key points |
|---|---|
| Current branch / dirty | status -sb |
| Default branch | origin/HEAD or main/master |
| fetch | git fetch origin --prune |
| worktree table | path · branch · dirty line count · locked? |
| merged local branches | git branch --merged origin/<default> |
| unmerged local branches | git branch --no-merged origin/<default> |
| current feature relative to default | git log --oneline origin/<default>..<branch> |
2. Return to the default branch
- Dirty → Stop, list the changes, ask the user
git checkout <default>git pull --ff-only origin <default>- Confirm that
status -sbis## <default>...origin/<default>and has no ahead/behind (or just state that it is aligned)
3. Classify each local branch / worktree
For each non-default local branch (and the worktree bound to it):
| Class | Condition | Action |
|---|---|---|
| SAFE | tip is an ancestor of origin/<default> ; worktree is not dirty and not locked |
Can delete the worktree → then branch -d |
| SQUASH_CANDIDATE | Not an ancestor, but the associated PR is already MERGED (gh pr view / merge commit message), or the user claims it is already merged |
Content verification is mandatory(see §4); only after it passes may you -D |
| KEEP | Has unique commits; or is dirty; or is locked; or cannot be proven to be on default | Keep, report commits ahead; when the user asks "should these have a PR added", go to §4.5 for a deep dive |
| DETACHED_TMP | worktree is at a detached HEAD and is located at /tmp、/private/tmp, an explicit review sandbox path |
If not dirty, you may worktree remove; there is no corresponding branch that needs to be kept |
4. Verification for squash / rebase merges (SQUASH_CANDIDATE)
branch --merged For squash will under-report. Before deleting, at least one of the following must be satisfied:
A. PR evidence (preferred)
gh pr list --state merged --head "<branch>" --json number,mergeCommit,url
# 或
gh pr view <n> --json state,mergedAt,mergeCommit
state=MERGED After that, B is still recommended.
B. Path/tree evidence (required when in doubt)
- List the paths changed by the branch relative to the merge-base:
git diff --name-only origin/<default>...<branch> - Fortask-relevantpaths (or all if the set is small): confirm that on
origin/<default>they exist and match the branch tip with no critical content gaps- Simple:
git diff origin/<default> <branch> -- <paths>is empty or contains only default updates - Or: the branch's unique paths already exist on default and the content is acceptable
- Simple:
- Forbiddento delete just because "the PR title looks similar" or "the issue is closed", so
-D
Verification failed → mark KEEP, and state what evidence is missing.
Efficiency tip (when there are many branches): do not run a separate gh pr viewfor each branch. First fetch everything in one go:
gh pr list --state merged --limit 500 --json headRefName,number,mergeCommit,mergedAt, headRefName locally to do dictionary matching. §4.A verification for N branches drops from N network calls to 1.
diff --stat noise trap (affects both KEEP / SQUASH decisions): running this on an old branch that has not been rebased for a long time
git diff origin/<default> <branch> --stat, the output will bedrowned by unrelated drift across the whole repository during that period(hundreds of files, tens of thousands of lines, all noise brought in by other PRs), making it completely impossible to see what this branch itself changed.
Correct approach: first git log origin/<default>..<branch> --oneline to find the commits unique to the branch,
then foreach of these commitsrun git show --stat <sha>(not the whole-branch diff) look at which files each one actually touched
— that is the branch's true change surface. When assessing KEEP ownership (see §4.5), always use this method; don't be intimidated by
--stat 's large numbers into misjudging "this branch changed half the repo."
4.5 Dig deep into KEEP: should this branch get a PR, has it already been superseded, or is someone already following up? (only when the user asks; not the default)
§3 tosses every branch that "has unique commits and can't be proven to be already on default" into KEEP and stops there—this is a safe default, not a final judgment. When the user asks "these look like legit tasks, should they get PRs," walk this decision tree for each KEEP branch, don't assume from the start that "no PR = should get a PR":
对每条 KEEP 分支:
1. gh pr list --head <branch> --state all --json number,title,state,mergedAt,headRefOid
2. 按结果分支:
PR 是 MERGED
→ 比对 PR 的真实 headRefOid 与本地分支 tip:
- 相同或本地是其祖先 → 本地无独有价值,安全删(内容已进 default,只是本地分支
对象本身没跟着更新)
- 本地 tip 领先于已合并的 PR head → 极少见但要处理:这些多出来的 commit 是
PR 合并*之后*才加的,按「从未有 PR」路径(步骤 4)单独复核这几条 commit
PR 是 CLOSED(非 merged)
→ 同名/同主题功能可能已经用**另一个分支**落地。核实手法:
`git log --oneline origin/<default> | grep -i "<PR 标题关键词>"`
或直接读 PR 标题描述的文件/功能是否已存在于 default。
确认后 = SUPERSEDED,删本地分支,报告里写清楚是被哪个 PR/commit 取代的
(不要只因为「PR 关了」就假设 superseded——必须找到真正承接内容的那个 commit)
PR 是 OPEN
→ **这条分支不是孤儿,是有人在管的活工作**。核实 `headRefOid` 是否等于本地 tip:
- 相等 → 本地就是那条 PR 分支的镜像,正常
- 不等 → 本地落后于远程 PR 真实状态(远程已经被继续推进),本地副本没有独有价值
本地一律**不需要**开新 PR(已经有一条在跟踪),也不要去改/推那条远程 PR 分支
(不知道谁在负责、可能撞车)。只报告状态 + 链接,交给已经在跑的流程处理。
从未有任何 PR(gh pr list 返回空)
→ 进入「补 PR 候选」判定,见下方 §4.6。绝不能只因为「没 PR」就假设是遗忘的好东西——
多数情况下没 PR 是因为内容早已用别的方式(甚至手工)落进 default 了,见 §4.6 的
冗余检测。
4.6 Redundancy check before filing a PR (mandatory for KEEP → never had a PR)
Core risk: treating "the local branch has a few more commits than default" directly as "this is good unsubmitted work"—this is a wrong assumption. Once a branch is old, what it changed has often already entered default via another path (another branch, a manual edit, piggybacked on another PR); it's just that this branch itself was never updated/deleted.Before filing a PR you must prove the content isn't already on default, you can't rely solely on the signal "has unique commits."
- Use the §4 "
diff --statnoise trap" method to find, from the branch's unique commits, which files itactually changed (not the hundreds of files from the whole-branch diff). - For each file docontent-levelredundancy checking, not path-level:
diff <(git show origin/<default>:<path>) <branch-or-worktree-working-copy>/<path>- identical → this file's change isalready on default(most likely brought in by another branch/manual edit), mark redundant
- different → check whether the diff content is the thing this branch itself is meant to do, or whether the branch is justbehinddefault on later unrelated changes (in that case the diff shows new content default has and the branch doesn't—this is the branch being "outdated" not the branch being "ahead," likewise not unique value worth submitting)
- The same logic applies to uncommitted changes in a dirty worktree—don't assume
git statusshowing dirty means it's valuable work in progress. Likewise do the step-2 content-level comparison for each dirty file: if the working copy's content matches the version on default, this is just "someone happened to manually duplicate an already-merged change," not independent work, and it can be discarded directly (follow normal dirty handling: show the user first before discarding, don'tgit checkout --/git cleanit away before confirming). - Anomalous dirty signals—suspect corruption first, not edits: if a worktree shows hundreds or thousands of files marked as
worktree deletions (especially repo-root-level files like
LICENSE.md/CHANGELOG.md, or entire unrelated directories likeplatforms//codereview/whose paths have nothing to do with the current task also being in the deletion list), thisis not a real edit, it means this worktree directory has been cleared or moved by an external process/sandbox/another tool. Do not analyze this kind of dirty state file by file — once you recognize this pattern, directly report "suspected directory corruption, not real changes"; if the branch's own content has already been confirmed to be merged elsewhere (went through the §4.5 MERGED path), you can, after user confirmationworktree remove --force; if the branch content has not yet been confirmed, first follow the normal KEEP process, and do not spend a large amount of time diffing thousands of files just because "it's dirty so it might be important". - Redundancy checks all pass, confirmed to be genuinely unique content → only then proceed to §4.7 to submit a supplementary PR.
4.7 Submit a supplementary PR (after §4.6 confirms the content is genuinely unique)
- rebase, do not merge:
git rebase origin/<default> <branch>(or first usegit branch <branch>-pr <branch>to make a copy and then rebase, keeping the original branch name unchanged, to avoid confusion with same-named PR records that may exist in history). Conflict → stop, report the conflicting files, do not unilaterally pick one side. - After a clean rebase, run verification: use the repository's own verification entry point (if there is an exemption mechanism such as
--na "docs-only change", pure documentation changes can use it; otherwise run the real verification gate). - Push the new branch, open a PR.The PR body must explicitly state that this is a "supplementary submission"— write clearly: why this piece of work previously did not go through a PR (if the reason is known), whether the content is still accurate/not outdated (what the current status of the related issue/epic is, whether it has been verified as not eliminated by subsequent changes), and generate the identity line according to the repository convention. Do not let the reviewer think this is a brand-new, spur-of-the-moment change.
- After the supplementary PR is opened, delete the old local branch (the content is already on the new PR branch, and the old branch name no longer needs to be kept).
5. Perform deletion (only SAFE + verified SQUASH)
Fixed order:
git worktree remove [--force 仅当用户确认且非 dirty 误报]<path>git branch -d <branch>; squash verified and usable-Dgit worktree prune
Never:
git push origin --delete(unless the user explicitly requests cleaning the remote)git clean -fdx/ delete untracked build artifacts (unless the user explicitly requests it)- Delete
main/ default / current branch
6. Report template
## git-hygiene · <repo path>
**default**: `<branch>` @ `<shortsha>` (= origin/…)
**working tree**: clean | dirty (… )
### Removed
- worktrees: …
- branches: … (SAFE / SQUASH#<pr>)
### Kept (still have unique work or risk)
| ref | ahead of origin/<default> | reason |
|-----|---------------------------|--------|
| … | N commits / dirty / locked | … |
### Not done
- …
When going through the §4.5/4.6/4.7 deep dive, add a section after the Kept table, giving the ownership determination conclusion item by item (do not just write "kept"):
### KEEP 深挖结论
| ref | 判定 | 证据 | 动作 |
|-----|------|------|------|
| … | SUPERSEDED(被 #123 取代)/ ACTIVELY-TRACKED(open PR #456)/ 已补交(PR #789)/ 冗余已丢弃 / 仍不确定 | … | 已删 / 已开 PR / 留给用户 |
With multiple repos, one section per repo.
Optional: local branches whose remote is gone
git fetch --prune after that, tracking shows gone:
- Still first classify per §3
- gone + SAFE (already an ancestor of default) → can delete
- gone + has unique commits → KEEP(may be unpushed or squash unverified)
Do not write script logic that says "if gone, delete all".
Relationship with wrap-up
- wrap-up: session review, documentation/profile sync
- git-hygiene: local Git graph reset
They can be chained in the same session: hygiene first then wrap-up, or the reverse; they do not replace each other.
What not to do
- Do not tidy the stash list (unless the user requests it)
- Do not close GitHub PRs / issues
- Do not upgrade dependencies, do not run the full test gate (unless the user requests verifying that main can build)
- Do not treat "the branch name looks like an agent temporary one" as a sufficient condition for deletion