Agent Skills: git-hygiene

>-

UncategorizedID: arcblock/agent-skills/git-hygiene

Install this agent skill to your local

pnpm dlx add-skill https://github.com/ArcBlock/agent-skills/tree/HEAD/plugins/agentloop/skills/git-hygiene

Skill Files

Browse the full folder contents for git-hygiene.

Download Skill

Loading file tree…

plugins/agentloop/skills/git-hygiene/SKILL.md

Skill Metadata

Name
git-hygiene
Description
>-

git-hygiene

收工 / 合并后的本地 Git 卫生:回默认分支 → 拉最新 → 只清确认无独特工作的 worktree 与本地分支。

与现有 skill 的分工:

| Skill | 做什么 | | --- | --- | | git-hygiene(本 skill) | 回 main、ff-only pull、按「相对 origin/main 是否还有独有提交」清 worktree/分支 | | git-worktree(compound-engineering) | 创建 / 切换 / 交互式清理 .worktrees/ 约定 下的并行 worktree | | blocklet-branch | 按 blocklet 仓库约定开迭代分支 |

本 skill 不替你 merge PR、不 force-push、不 git clean -fdx、不丢 dirty 改动。

触发

  • 「切到 main / 拉最新 / 清理没用的 worktree 和 branch」
  • 「确认没有未合并工作再删」
  • PR 已 merge 后的本地收尾
  • 多 agent 并行结束后的主工作区复位
  • 「这些 KEEP 的分支是不是该补 PR / 帮我看看最新改动是什么时候 / 仔细核对没合入的话都发 PR」 → 触发 §4.5-4.7 深挖流程(默认 §3-6 主流程不做这一层,太贵;只在用户明确要求时才逐条调查)

默认假设

  • 远程名:origin
  • 默认分支:优先 origin/HEAD → 否则 main → 否则 master
  • 删除前提 = 内容已在 origin/<default> 上,不是「我记得 merge 了」

硬规则

  1. 主工作区 dirty → 停。 有 uncommitted 改动时不 checkout、不删分支。先让用户 commit / stash / 明确丢弃。
  2. 只 git pull --ff-only。 拒绝非快进;有分叉就报告,不 rebase/merge 擅自修。
  3. 不删仍有独有提交的分支。
    git merge-base --is-ancestor <branch> origin/<default> 为假 → KEEP,除非走 squash 校验(见下)。
  4. 不删 dirty / locked worktree。
  5. 不删当前检出分支(先切到 default)。
  6. 主 worktree 永不 worktree remove。
  7. 远程 origin/<branch> 已 gone ≠ 本地可删。 gone 只说明远端没了;本地 tip 仍可能有未合入提交。
  8. 一次审批只管这一次。 用户说「继续清」时仍对 KEEP 列表逐类处理,不默认 branch -D 全部 unmerged。

流程

0. 范围

  • 默认:当前 repo(git rev-parse --show-toplevel)
  • 用户点名多个 repo(如 site + arc):每个 repo 单独跑完整流程,报告分开写

优先跑确定性脚本(inventory;可选 apply)。脚本在 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

Agent 必须读脚本输出再决策;apply 只处理 SAFE 类。SQUASH_CANDIDATE / KEEP 由 agent 按下面规则处理。

1. 库存(只读)

收集并展示:

| 项 | 命令要点 | | --- | --- | | 当前分支 / dirty | status -sb | | 默认分支 | origin/HEAD 或 main/master | | fetch | git fetch origin --prune | | worktree 表 | path · branch · dirty 行数 · locked? | | merged 本地分支 | git branch --merged origin/<default> | | unmerged 本地分支 | git branch --no-merged origin/<default> | | 当前 feature 相对 default | git log --oneline origin/<default>..<branch> |

2. 回默认分支

  1. Dirty → 停,列出改动,问用户
  2. git checkout <default>
  3. git pull --ff-only origin <default>
  4. 确认 status -sb 为 ## <default>...origin/<default> 且无 ahead/behind(或仅说明已对齐)

3. 分类每个本地分支 / worktree

对每个非 default 本地分支(及绑定它的 worktree):

| 类 | 条件 | 动作 | | --- | --- | --- | | SAFE | tip 是 origin/<default> 的祖先;worktree 非 dirty、非 locked | 可删 worktree → 再 branch -d | | SQUASH_CANDIDATE | 非祖先,但关联 PR 已 MERGED(gh pr view / 合并提交信息),或用户声称已 merge | 必须做内容校验(见 §4)通过后才可 -D | | KEEP | 有独有 commit;或 dirty;或 locked;或无法证明已在 default 上 | 保留,报告 commits ahead;用户要求「这些是不是该补 PR」时走 §4.5 深挖 | | DETACHED_TMP | worktree 为 detached HEAD,且位于 /tmp、/private/tmp、明确的 review 沙箱路径 | 非 dirty 则可 worktree remove;无对应需保留分支 |

4. Squash / rebase 合入的校验(SQUASH_CANDIDATE)

branch --merged 对 squash 会漏报。删除前至少满足其一:

A. PR 证据(优先)

gh pr list --state merged --head "<branch>" --json number,mergeCommit,url
# 或
gh pr view <n> --json state,mergedAt,mergeCommit

state=MERGED 后仍建议 B。

B. 路径/树证据(必做当有疑虑)

  1. 列出分支相对 merge-base 改过的路径:
    git diff --name-only origin/<default>...<branch>
  2. 对任务相关路径(或全部若集合小):确认 origin/<default> 上存在且与分支 tip 无关键内容缺口
    • 简单:git diff origin/<default> <branch> -- <paths> 为空或仅 default 更新
    • 或:分支独有路径在 default 上已存在且内容可接受
  3. 禁止仅因「PR 标题像」或「issue 关了」就 -D

校验失败 → 标 KEEP,写明缺什么证据。

效率提示(分支数量多时):不要对每个分支单独 gh pr view。先一次性拉全量: gh pr list --state merged --limit 500 --json headRefName,number,mergeCommit,mergedAt, 本地用 headRefName 做字典匹配。N 个分支的 §4.A 查证从 N 次网络调用降到 1 次。

diff --stat 噪音陷阱(KEEP / SQUASH 判断都会踩):对一个很久没 rebase 的旧分支跑 git diff origin/<default> <branch> --stat,输出会被整个仓库这期间的无关漂移淹没 (几百个文件、几万行,全是别的 PR 带来的噪音),完全看不出这个分支自己的改动是什么。 正确做法:先 git log origin/<default>..<branch> --oneline 找出分支自己独有的 commit, 再对这些 commit 各自跑 git show --stat <sha>(不是分支整体 diff)看它们各自动了哪些 文件——这才是这个分支的真实改动面。診断 KEEP 的产权时(见 §4.5)一律用这个方法,不要被 --stat 的大数字吓退或误判「这分支改了半个仓库」。

4.5 深挖 KEEP:这条分支该补 PR、已经 superseded、还是已经有人在跟?(用户要求时才做,非默认)

§3 把「有独有 commit 且证不了已在 default 上」的分支统统扔进 KEEP 就停了——这是安全默认, 不是终局判断。用户问「这些看起来是正经任务,是不是该补 PR」时,对每条 KEEP 分支走这个决策树, 不要一上来就假设「没 PR = 该补 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 补 PR 前的冗余检测(KEEP → 从未有 PR 时必做)

核心风险:把「本地分支比 default 多几个 commit」直接当成「这是没提交的好工作」——这是 错的假设。分支旧了以后,它改的内容常常已经用别的路径(另一个分支、手工改、别的 PR 顺带改) 进了 default,只是这条分支自己没被更新/删除。补 PR 前必须证明内容还没在 default 上, 不能只凭「有独有 commit」这个信号。

  1. 用 §4「diff --stat 噪音陷阱」的方法,从分支独有 commit 里找出它真正改了哪些文件 (不是整条分支 diff 出来的几百个文件)。
  2. 对每个文件做内容级冗余检测,不是路径级:
    diff <(git show origin/<default>:<path>) <branch-or-worktree-working-copy>/<path>
    
    • 完全相同 → 这个文件的改动已经在 default 上(大概率是别的分支/手工改带进去的), 标记冗余
    • 不同 → 看 diff 内容是不是这条分支自己要做的那件事,还是分支只是落后于 default 后来的无关改动(这种情况下 diff 会显示 default 有、分支没有的新内容——这是分支 「过时」不是分支「领先」,同样不算需要补交的独有价值)
  3. 同一逻辑适用于 dirty worktree 里的未提交改动——不要因为 git status 显示 dirty 就默认那是有价值的在途工作。同样对每个 dirty 文件做步骤 2 的内容级比对:如果 working copy 的内容跟 default 上的版本一致,这只是「凑巧手工重复了一遍已经合并的改动」,不是 独立工作,直接可以丢弃(走正常 dirty 处理:先给用户看一眼再丢,不要在没确认前 git checkout -- / git clean 掉)。
  4. 反常 dirty 信号,先怀疑损坏不是编辑:如果一个 worktree 显示成百上千个文件被标记为 工作区删除(尤其是仓库根级文件如 LICENSE.md/CHANGELOG.md、或整个不相关目录如 platforms//codereview/ 之类跟当前任务八竿子打不着的路径也在删除列表里),这不是 真实编辑,是这个 worktree 目录被外部进程/沙箱/别的工具清空或挪用过。不要逐文件分析 这类 dirty——识别出这个模式后直接报告「疑似目录损坏,非真实改动」,如果分支本身内容已 确认在别处合并(走了 4.5 的 MERGED 路径),可以在用户确认后 worktree remove --force; 如果分支内容还没确认过,先按正常 KEEP 流程走,不要因为「dirty 所以可能重要」而投入 大量时间去 diff 几千个文件。
  5. 冗余检测全部通过、确认是真实独有内容 → 才进 §4.7 提交补 PR。

4.7 提交补 PR(§4.6 确认内容真实独有后)

  1. rebase 不 merge:git rebase origin/<default> <branch>(或先 git branch <branch>-pr <branch> 复制一份再 rebase,保留原分支名不动,避免和历史上可能存在的 同名 PR 记录混淆)。冲突 → 停,报告冲突文件,不擅自二选一。
  2. 干净 rebase 后跑改动包的测试(纯文档改动无需跑),PR 正文写确切命令 + 计数。
  3. push 新分支,开 PR。PR body 必须显式说明这是「补交」——写清楚:为什么这条工作 之前没走 PR(如果知道原因)、内容是否仍然准确/未过时(关联 issue/epic 当前状态如何, 有没有验证过没被后续变化淘汰)、身份行按仓库约定生成。不要让 reviewer 以为这是一个 全新的、临时起意的改动。
  4. 补 PR 开出后,删掉本地旧分支(内容已经在新 PR 分支上,旧分支名不再需要保留)。

5. 执行删除(仅 SAFE + 已校验 SQUASH)

顺序固定:

  1. git worktree remove [--force 仅当用户确认且非 dirty 误报] <path>
  2. git branch -d <branch>;squash 已校验可用 -D
  3. git worktree prune

绝不:

  • git push origin --delete(除非用户明确要求清远程)
  • git clean -fdx / 删未跟踪构建产物(除非用户明确要求)
  • 删 main / default / 当前分支

6. 报告模板

## 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
- …

走了 §4.5/4.6/4.7 深挖时,在 Kept 表格后加一节,逐条给出产权判定结论(不要只写「kept」):

### KEEP 深挖结论
| ref | 判定 | 证据 | 动作 |
|-----|------|------|------|
| … | SUPERSEDED(被 #123 取代)/ ACTIVELY-TRACKED(open PR #456)/ 已补交(PR #789)/ 冗余已丢弃 / 仍不确定 | … | 已删 / 已开 PR / 留给用户 |

多 repo 时每个 repo 一节。

可选:远程 gone 的本地分支

git fetch --prune 后 tracking 显示 gone:

  • 仍先按 §3 分类
  • gone + SAFE(已是 default 祖先)→ 可删
  • gone + 有独有 commit → KEEP(可能是未推完或 squash 未校验)

不要写「gone 就全删」的脚本逻辑。

与 wrap-up 的关系

  • wrap-up:session 回顾、文档/profile 同步
  • git-hygiene:本地 Git 图复位

可同一会话串联:先 hygiene 再 wrap-up,或相反;互不替代。

不做什么

  • 不整理 stash 列表(除非用户要求)
  • 不关闭 GitHub PR / issue
  • 不升级依赖、不跑全量测试登录校验(除非用户要求验证 main 可构建)
  • 不把「分支名像 agent 临时」当成可删充分条件