LobeHub Git Worktree 清理技能:确定性审计与陈旧本地分支的安全清理
在多 worktree 协作的开发模式下,长期积累的"已完成合并"或"远端已被删除"的本地分支会不断侵蚀 git worktree list 的可读性。LobeHub 在仓库的 Agent 技能体系中提供了一个名为 cleanup-git-worktrees 的技能(SKILL.md),它用一份确定性脚本来完成"审计—分类—审批—清理—复验"的完整闭环。读完本文,你可以掌握该技能的分类语义、audit/clean 两个子命令的全部参数,以及其底层 Bash 实现中的关键守护逻辑。
一、为什么 LobeHub 需要这个技能
LobeHub 的分支策略决定了 worktree 的周转速度。根据 AGENTS.md 中 "Git Workflow" 一节:
canary是开发分支(对应云端生产环境),main是发布分支(定期从 canary cherry-pick);- 新分支应从
canary创建,PR 目标是canary; - 分支命名格式为
<type>/<feature-name>,即典型的feat/xxx、fix/xxx短生命周期分支。
短生命周期分支 + 并行开发意味着大量 worktree 在完成 PR 后不再使用。这些残留 worktree 的问题在于:
git worktree list输出越来越长,无法一眼看出哪些可以清理;- 直接
git branch -d会因"未合并"(相对main而言)而失败——因为 LobeHub 的合并目标是canary而非main; - 远端分支被 squash 合并后删除,本地分支的 upstream 会变成
[gone],但[gone]并不等于已合并(rebase 合并同理),必须区分处理。
该技能正是为解决这三点而设计的:它把"哪些 worktree 可以安全删除"这个模糊判断,收敛为一组可枚举、可复现的分类标签。
二、技能的结构与设计哲学
技能目录 cleanup-git-worktrees/ 包含三个文件:
| 文件 | 职责 |
|---|---|
| SKILL.md | 面向 Agent 的操作规程:工作流、分类语义、安全红线 |
| scripts/cleanup.sh | 唯一的执行入口,216 行 Bash,负责确定性的解析与守护 |
| agents/openai.yaml | 技能对 Agent 的接口元数据(显示名、默认提示词) |
SKILL.md 开篇就定下总原则(原文明确要求):使用捆绑脚本让分类保持确定性,不要临时手搓解析和守护逻辑("Use the bundled script to make classification deterministic … do not recreate its parsing and guard logic ad hoc");同时把清理视为破坏性操作——先审计、展示精确候选项、获得用户明确批准后再执行删除,除非用户当前请求已经点名了具体目标。
agents/openai.yaml 提供了技能发现所需的元数据:
interface:
display_name: 'Cleanup Git Worktrees'
short_description: 'Safely audit and clean completed Git worktrees'
default_prompt: 'Use $cleanup-git-worktrees to audit and safely remove completed worktrees and stale local branches.'
三、audit 子命令:只读审计与参数细节
3.1 用法
脚本头部的 usage 定义了两种调用形态(cleanup.sh#L5-L13):
cleanup.sh audit [--fetch] [--base <ref>]
cleanup.sh clean [--base <ref>] --branch <name> [--branch <name> ...] [--apply]
标准审计命令(SKILL.md 推荐写法):
bash .agents/skills/cleanup-git-worktrees/scripts/cleanup.sh audit --fetch --base origin/canary
参数语义如下:
--base <ref>:作为"已合并"判断基准的 ref。脚本默认值即origin/canary(cleanup.sh#L31),与 LobeHub 的 PR 目标分支一致。执行前脚本会校验该 ref 存在——依次检查refs/remotes/<base>与refs/heads/<base>,都不存在则直接报错退出(cleanup.sh#L66-L68)。--fetch:触发git fetch --prune <remote>,其中 remote 从 base ref 的前缀解析(origin/canary→origin,无前缀则回退为origin),见 cleanup.sh#L70-L74。--prune是关键:它会同步清理本地已失效的远端跟踪引用,使[gone]标记真实反映远端状态。只有在网络不可用时才应省略--fetch,且必须向用户披露"远端状态可能已过期"(SKILL.md 第 18 行原文要求)。
除可选的 fetch 外,audit 是完全只读的。
3.2 输出格式:两个 scope 的 TSV 报告
audit 输出的是一张 TSV 表,表头为(cleanup.sh#L139):
scope path branch dirty upstream track merged_into_base classification
字段含义:
| 字段 | 来源 | 说明 |
|---|---|---|
scope |
— | worktree(绑定 worktree 的分支)或 branch(游离本地分支) |
path |
git worktree list --porcelain |
worktree 目录;游离分支显示 - |
branch |
解析 branch refs/heads/ 行 |
本地分支名 |
dirty |
`git -C | wc -l` |
upstream |
git for-each-ref 的 %(upstream:short) |
配置的 upstream,无则 none |
track |
%(upstream:track) |
领先/落后情况;upstream 已消失时为 [gone] |
merged_into_base |
git merge-base --is-ancestor <branch> <base> |
yes/no |
classification |
见下节 | 七个标签之一 |
audit 的枚举分两遍(cleanup.sh#L138-L180):第一遍用 awk 解析 git worktree list --porcelain 的块状输出(RS="" 分块、提取 worktree 与 branch refs/heads/ 两行),列出所有"分支名已绑定到某个 worktree"的条目,并用临时文件记录这些分支名;第二遍遍历 git for-each-ref refs/heads,用 grep -Fxq 跳过已绑定的,只报告剩余的自由分支——因此一张表能同时回答"哪些 worktree 可删"和"哪些游离分支可删"。
四、七个分类标签:classify() 的判定优先级
分类是整个技能的核心。classify() 按固定优先级从上到下短路求值,顺序本身就是安全策略的一部分:
protected-branch → protect-current → protect-dirty → candidate-merged → candidate-gone → review-no-upstream → active
各标签的判定实现与 SKILL.md 的语义对照如下:
| 标签 | 判定条件(源码) | 语义 |
|---|---|---|
protected-branch |
分支名为 main、canary,或等于 base ref 全名/短名(is_protected_branch()) |
永不删除。即使 origin/main 上的内容与 canary 高度重合,脚本也不允许通过本路径触碰主干 |
protect-current |
worktree 路径经 pwd -P 规范化后与当前 CWD 相同(cleanup.sh#L123) |
保护正在运行本命令的 worktree,防止自删 |
protect-dirty |
dirty > 0 |
存在已修改或未跟踪文件,保留。脏文件数来自 git status --porcelain 的行数 |
candidate-merged |
git merge-base --is-ancestor <branch> <base> 成功(is_merged()) |
干净且完全包含于 base 分支——这是最强清理候选 |
candidate-gone |
%(upstream:track) 输出 [gone] |
干净且配置的 upstream 已被 prune。注意 SKILL.md 原文强调:这是陈旧,但不是 squash/rebase 合并后的已合并证明 |
review-no-upstream |
无 upstream 且未被 base 包含 | 干净但需要人工判断,进入 review 队列 |
active |
其余情况 | 保留,除非用户明确声明该分支已完成 |
注意两个关键实现细节:
- 优先级即护栏。
protect-dirty排在candidate-merged之前——一个分支即使已完全合并进 canary,只要工作区有未提交改动就绝不进入候选集,这与 SKILL.md 安全规则"绝不能因为分支已合并或[gone]就丢弃脏 worktree"一一对应。 [gone]与 merged 是两个独立信号。audit报告把它们放在不同列(track与merged_into_base),SKILL.md 也要求呈现结果时把candidate-merged与candidate-gone分开列示,"不要把[gone]描述成已合并";当这一区分真的影响决策时,应去查 PR 平台的实际状态(原文建议查 GitHub PR state)。
按 SKILL.md 第 30 行,向用户呈现审计结果时应给出一张紧凑表格,至少包含:path、branch、dirty 数、upstream 状态、base 包含关系、分类,并遵守上述"merged 与 gone 分列"的要求。
五、clean 子命令:dry-run 默认与删除前再审计
获得批准后,用精确分支名调用 clean(SKILL.md 第 34-40 行示例):
bash .agents/skills/cleanup-git-worktrees/scripts/cleanup.sh clean \
--base origin/canary \
--branch feat/example \
--branch fix/example \
--apply
clean() 的执行流程:
- 参数校验:至少一个
--branch,缺失即die 'clean requires at least one --branch';每个分支名必须对应真实存在的本地分支(git show-ref --verify),否则整体中止。 - 删除前即时再审计:这是脚本最重要的守护。对每个目标分支,clean 会重新解析其绑定的 worktree(branch_worktree())、重新统计 dirty 数、重新调用同一个
classify()。这与 SKILL.md"脚本会在删除前对每个目标重新审计"的描述一致——从审计到删除之间的时间窗口内,分支状态若发生任何变化(比如被 push 了新提交、工作区变脏),分类结果会随之改变,从而被第 3 步拦截。 - 白名单拦截:只允许
candidate-merged与candidate-gone通过,其余任何分类(包括protect-dirty、active、review-no-upstream)都会触发die "<branch> is <classification>; refusing cleanup"并整体退出。换言之,无法通过该脚本强制删除"既未包含于 base 也非 [gone]"的分支,这正是 SKILL.md 安全规则第 3 条的落地。 - dry-run 默认:不带
--apply时逐行输出DRY-RUN <branch> <worktree|-> <classification>后结束,不产生任何变更;带--apply才执行真实删除。 - 删除动作:先
git worktree remove <path>(若有绑定 worktree),再git branch -D <branch>,并分别输出REMOVED-WORKTREE/REMOVED-BRANCH标记行,便于自动化解析结果。
选择 git worktree remove 而非目录级递归删除不是随意的:git worktree remove 会同时注销 worktree 注册信息,而绕过它直接删目录会留下需要 git worktree prune 才能回收的"孤儿注册"。SKILL.md 安全规则对此有明确禁令:"绝不使用递归删除来绕过 git worktree remove。如果 Git 部分删除了目录,停下并检查确切路径,再决定如何恢复。"
六、完整安全规则(原文继承)
SKILL.md 的 "Safety rules" 一节共 6 条,是执行该技能时必须全程遵守的红线:
- 绝不能因为分支已合并或
[gone]就丢弃脏 worktree; - 绝不用递归删除绕过
git worktree remove;若 Git 部分删除了目录,立即停下并检查确切路径后再决定恢复方式; - 绝不通过本脚本强制删除"既不在 base 内、也非
[gone]"的分支; - 上游被删除表示陈旧,而非必然已合并;当区分二者影响决策时使用 PR 平台状态;
- 保留无关的用户改动与并行的 worktree;
- 分类前优先执行
git fetch --prune origin;git worktree prune只用于目录已经消失的注册项。
其中最后一条解释了脚本内没有自动执行 git worktree prune 的原因:prune 只应针对"目录已缺失"的注册项,属于状态修复而非候选删除,是否执行由操作者按实际情况决定。
七、推荐的五步工作流
综合 SKILL.md 的 "Workflow" 一节,一次完整的清理应遵循:
- 审计:在仓库任意 worktree 内执行
audit --fetch --base origin/canary;无网络时省略--fetch并披露远端状态可能过期; - 解读分类:按第四节的语义表理解每个标签,注意
candidate-gone不等于已合并; - 呈现候选:给出含 path / branch / dirty / upstream 状态 / base 包含关系 / 分类的紧凑表格,
candidate-merged与candidate-gone分列; - 批准后清理:把精确分支名传给
clean,不加--apply先跑一次 dry-run 核对输出,再加--apply执行; - 复验并汇报:再次运行
audit,报告——已删除的 worktree 与分支、被保留的 dirty/current 目标、剩余的 worktree 与本地分支数量、以及任何部分删除或 Git 错误。
八、小结:确定性、幂等与最小破坏面
这个技能的价值不在于命令本身(git worktree remove + git branch -D 并不复杂),而在于把清理决策工程化:
- 确定性:分类完全由脚本从
git原始输出推导,同一状态下重复执行得到同一结论,Agent 无需"凭感觉"判断; - 最小破坏面:默认 dry-run、删除前再审计、白名单式放行、保护链(protected → current → dirty)全部排在候选链之前;
- 可审计:TSV 报告与
DRY-RUN/REMOVED-*标记行都是机器可读的,清理结果可复验、可追溯。
对于采用 LobeHub 这种"canary 开发 + 短生命周期功能分支 + 多 worktree 并行"的团队,这套"先分类、后批准、再删除、终复验"的模式可以直接作为其他仓库 worktree 治理的参考模板。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00