news 2026/9/28 6:26:26

gsd-core Worktree Cleanup-Wave 已移除目录容错:基于 git 注册表与 ENOENT 的波次清理语义

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
gsd-core Worktree Cleanup-Wave 已移除目录容错:基于 git 注册表与 ENOENT 的波次清理语义

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址:https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看免费下载

本指南围绕 gsd-core 的worktree cleanup-wave清理机制展开,讲解其在一波并行 executor 收尾时,如何正确对待「工作树目录已被 harness 提前移除」这一常规情形:合并照常进行、陈旧的管理条目由 teardown 剪除,而不是让整波清理失败。读完你将掌握 cleanup-wave 的身份(identity)与移除(removal)判定边界、波前快照的作用,以及该行为对应的源码实现与测试证据。

引言:一次修复改变了整波清理的语义

在 gsd-core 的 execute-phase 与 quick 工作流中,多个 executor 以 git worktree 形式并行运行,每个 executor 完成后,编排器(orchestrator)需要把其分支合并回主线、再清理对应的 worktree 与临时分支。这一收尾动作被收敛为 SDK 命令worktree.cleanup-wave(调用点、quick 工作流),调用时以--manifest传入一波 executor 的清单,并以|| exit 1保证失败闭合(fail-closed)。

本次修复(changeset 类型Fixed,关联 PR 4612)解决了一个此前会让整波清理「卡死」的边界情形:当某个条目对应的 worktree 目录已被 harness(如 Claude Code 在子代理完成后立即清理其工作树)提前移除时,旧逻辑因git -C <gone-path>读取失败而将其误判为branch_mismatch并阻断合并,导致该条目无法合回主线,且其余条目也连带被搁置。修复后,只要 git 的注册信息仍把该路径绑定到清单所声明的分支,且该路径经statSync确认确实已消失(ENOENT),该条目就会被正常合并,其陈旧的管理条目由 teardown 阶段的git worktree prune剪除。

两个问题、两个证据源:身份与移除必须分开回答

cleanup-wave 对每个条目都要回答两个截然不同的问题,而源码 worktree-safety.cts 明确注释了「两个问题、两个来源,每个只回答它真正能证明的那一个」:

  • 身份(IDENTITY)——「注册在这个路径上的 checkout 是不是清单所声明的分支?」答案来自git worktree list --porcelain。git 永远不会丢掉这条绑定:即使目录被rm -rf,porcelain 输出仍会打印worktree <path>与branch refs/heads/<branch>。
  • 移除(REMOVAL)——「这个目录是真的没了,还是仅仅不可读?」答案来自statSync的 errno。只有ENOENT才代表「已移除」;EACCES、EIO等其他 errno 一律按「不可读」处理,照旧阻断。

这两个来源互不越权,并且两个方向的误判都被实测验证过(源码注释中明确标注「measured rather than reasoned about」):

  1. 早期实现用fs.existsSync返回 false 推断移除,并在没有 checkout 可读时回退到refs/heads/<branch>做身份判定,这把身份从「注册在此的 checkout 在该分支上」弱化为「存在一个同名的分支」,从而可能放行一个无关的兄弟分支。
  2. prunable并不是移除测试:实测中,当父目录权限为 000 时,git 会对一个仍然存在的 checkout 输出prunable gitdir file points to non-existent location——它无法遍历父目录,因而误报 gitdir 文件缺失。若把prunable当作「已移除」接受,就会在一个不可读的 worktree 上合并未提交的工作,而这正是 dirty 检查要拒绝的场景。statSync能把二者分开:ENOENT 是「没了」,EACCES/EIO 是「不可读」。

代码层面的实现即confirmedGone(worktree-safety.cts):它只认errno === 'ENOENT',路径能成功stat即视为存在,任何其他 errno 一律视为不可读并保持阻断。

波前快照:一次 prune 不会搁置其余条目

git worktree prune是仓库级的维护操作,它会清除所有陈旧的管理条目,而不只是当前这一个。实测表明:两个已移除的 worktree,执行一次prune,两个注册条目都会消失。因此,如果每个条目都在循环内重新读取git worktree list,那么第一个被 harness 移除的条目完成 teardown 后,后续条目的注册证据已经没了,会被误判为branch_mismatch而全部阻断——这比修复前的 bug 更糟,因为「一波并行 executor」恰恰是常态。

修复的关键设计是惰性波前快照(worktree-safety.cts):

  • 在第一个真正需要身份判定的条目处,一次性读取worktree list --porcelain并缓存为worktreeListSnapshot;
  • 之后整波条目复用这份快照,快照早于任何 teardown 的 prune,因此每个条目的注册信息都以「剪除之前」的状态为准;
  • 惰性(lazy)保证 happy path 不额外开销:一波 worktree 全部在场时,根本不会触发这次子进程读取。

测试 worktree-safety.test.cjs 精确覆盖了这一点:两个条目,第一个已被移除(触发 prune),第二个仍要合并——断言两个条目的状态都是merged_removed,pending为空。

缺失目录的接受路径:合并照常,警告照发

当git -C <worktree> rev-parse --abbrev-ref HEAD读取失败时,修复不再直接阻断,而是先询问 git 与文件系统「为什么」:只有「git 仍把该路径绑定到清单声明的分支且目录经statSync确认 ENOENT」才放行(absentAndIdentified)。任何其他失败形态都是真实的失配:

  • 该路径注册了别的分支——正是防分支调换(swap)控制要拦截的;
  • git 完全没有列出该路径——清单点名了 git 无记录的东西;
  • 路径能stat成功,或stat失败但 errno 不是 ENOENT——checkout 存在或不可读,修复前阻断、修复后依旧阻断。

值得注意的是,接受路径是在失败点事后判定(disambiguate at the point of failure)而非提前预判:一个成功读取的 present worktree 走原有逻辑,分支不符照样branch_mismatch阻断,行为完全不变。

由于「harness 干净移除了已完成 executor」与「操作者或外部进程移除了该路径」在代码面前是同一个签名(git 注册仍在、statSync同为 ENOENT),接受常规情形若保持静默,会让非常规情形失去唯一的报警信号。因此接受必定伴随一条咨询性(advisory)警告ACCEPTED_ABSENT_WORKTREE(警告代码定义),它绝不作为闸门:条目照常合并,但会把 git 自己的prunable原因原文附上,供操作者调查。测试断言该警告携带detail: 'gitdir file points to non-existent location'而非转述(worktree-safety.test.cjs);git 某些版本只输出裸prunable标记时,detail为null(同文件 L2773-L2796)。

teardown 阶段的额外护栏:prune 而非强删,且须复查 ENOENT

接受为缺失的条目,在合并完成后走的是git worktree prune剪除管理条目,而不是git worktree remove --force——因为强删会删除可能已重新出现在该路径、且从未通过 rescue/dirty 检查的内容(worktree-safety.cts)。两个护栏值得注意:

  1. 删前复查:从身份判定到合并落地之间是一个宽窗口(base 门、deletion 门、scope 门、合并本身都可能耗时),worktree 可能在此期间重新出现。因此 teardown 前会再做一次confirmedGone,若路径已重现,则报worktree_remove_failed并拒绝 prune 与删分支——修复前的 bug 只是阻断,这里要防的是不可恢复的状态破坏。
  2. prune 的副作用被快照吸收:注释明确纠正过「prune 只影响本条目」的错误说法——正因为 prune 会清掉同一波后续条目所需的注册证据,身份读取才必须前置为波前快照。

对在场的 worktree,teardown 仍是git worktree remove --force(锁定的先worktree unlock再重试);若 remove 因路径已消失而失败,则按absentAndIdentified判据决定是否改走 prune(worktree-safety.cts)。

完整判定流水线:一个条目的八道关卡

综合源码 executeWorktreeWaveCleanupPlan,一个清单条目在整波循环中依次经过:

  1. 身份判定:git -C <path> rev-parse --abbrev-ref HEAD输出须等于清单branch;失败时按上述absentAndIdentified判据决定「接受为缺失」或branch_mismatch阻断(隔离)。
  2. base 校验:git merge-base HEAD <branch>须命中expected_base或allowed_bases(#1265),否则base_mismatch阻断。
  3. 删除审计:git diff --diff-filter=D --name-only HEAD...<branch>检出删除,仅清单declared_deletions声明过的路径被豁免,其余以branch_contains_deletions阻断(#3003)。
  4. scope 咨询:分支实际变更若超出files_modified声明的范围,发出scope_out_of_declared咨询,不阻断(#2596)。
  5. SUMMARY 救援:把 worktree 下.planning/*-SUMMARY.md未提交产物先复制回主线(git cat-file -e HEAD:<path>判定是否已提交),救援失败则summary_rescue_failed阻断(worktree-safety.cts)。
  6. dirty 检查:git status --porcelain --untracked-files=all,已救援的 SUMMARY 路径从输出中过滤,剩余脏行以worktree_dirty阻断;此处同样在失败点用absentAndIdentified兼容「检查期间目录又被 harness 移除」的窗口。
  7. 合并:git merge <branch> --no-ff --no-edit,超时判merge_timed_out(hook 运行时长的独立预算见 DEFAULT_MERGE_TIMEOUT_MS),失败先merge --abort再以rev-parse --verify MERGE_HEAD判定仓库是否仍处于合并中(repoRootStillMidMerge),是则把后续条目移入pending并中止整波(这是唯一的仓库级失败豁免,#2852);被 kill 的合并还要通过git reset --merge+stash pop --index恢复合并残留(restoreMergeResidue)。
  8. teardown:在场走 remove/unlock/remove,缺失走 prune,最后git branch -D删分支;删分支失败降级为warning状态而非阻断。

每道失败关卡都遵循「隔离(isolate)」原则:单条目问题只影响该条目,其余条目继续独立评估;只有仓库级合并状态才break整波。

工作流中的实际调用方式

  • execute-phase:编排器在每个 executor 返回后,用gsd_run query worktree.record-agent把{agent_id, worktree_path, branch, expected_base, files_modified, declared_deletions}逐条写入WAVE_WORKTREE_MANIFEST(execute-phase.md),波次收尾统一调用:
gsd_run query worktree.cleanup-wave --manifest "$WAVE_WORKTREE_MANIFEST" || exit 1
  • quick:同样以QUICK_WORKTREE_MANIFEST走worktree.cleanup-wave,且测试强制要求|| exit 1的失败闭合语义,SDK 的安全拒绝必须浮出水面,不得被软回退吞掉(worktree-cleanup.test.cjs)。

清单的初始化形态为{"orchestrator_root": "...", "worktrees": []}(也接受裸数组),由worktree record-agent做写时校验:字段缺失、分支不符合^(worktree-)?agent-|worktree-wf_命名空间、重复(worktree_path, branch)都会大声失败并给出恢复提示,确保写入即能被 cleanup-wave 读回(planWorktreeRecordAgent)。

验证矩阵:测试如何锁定新语义

worktree-safety.test.cjs 为本次修复建立了完整的判定矩阵:

场景判定测试位置
两个已移除条目,第一个触发 prune两个都merged_removed(快照生效)L2720-L2742
缺失但注册匹配 + ENOENT合并 +ACCEPTED_ABSENT_WORKTREE咨询(带 git 原文)L2744-L2771
裸prunable标记(无原因文本)接受,detail: nullL2773-L2796
worktree list读不到(exit 128)branch_mismatch阻断(无注册证据即无身份)L2798-L2809
真实缺失(git 标记 prunable)merged_removed,不误伤L2811-L2824

结合 worktree-cleanup.test.cjs 对工作流层的约束(manifest 作用域、|| exit 1、record-agent 写时校验),本次修复形成了「SDK 判定 + 工作流契约 + 回归测试」三层闭环:常规的 harness 提前清理不再卡住整波,而任何真实失配(分支调换、不可读目录、未声明删除、脏工作树)依旧按原语义阻断——这正是本 changeset 全部四句语义承诺的落点。

小结

worktree cleanup-wave的本次修复把「目录没了」从「一律阻断」细化为「有条件接受」:身份继续由 git 自己的工作树注册信息提供(git worktree list --porcelain),移除只认statSync的 ENOENT;波前快照让一次仓库级prune不再搁置后续条目;接受路径伴随ACCEPTED_ABSENT_WORKTREE咨询警告并保留 git 原始原因;teardown 对缺失条目只 prune 不强删,且删前复查 ENOENT。整个判定链在 src/worktree-safety.cts 中可逐行追溯,并由 tests/worktree-safety.test.cjs 与 tests/worktree-cleanup.test.cjs 双向锁定。

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址:https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看免费下载
上一篇:SUSFS4KSU-Module配置指南:自定义你的root隐藏策略
下一篇:告别N+1查询:Goldiloader让Rails自动按需预加载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 6:26:18

廊坊网站关键词优化避坑指南

廊坊网站从零搭建SEO优化:3个避坑点让排名起飞 域名买好了,服务器也租了,结果网站打开慢得像蜗牛,百度搜半天连个影子都看不到。这种“域名服务器搞不懂”的困境,在廊坊做企业官网的老板身上太常见了。很多同行以为只要把页面做漂亮就行,却忽略了从零搭建阶段的技术选型,直接导致后期SEO优化事倍功半。…

作者头像 李华
网站建设 2026/9/28 6:26:02

做网站怎么租个域名:保姆级建站教程避坑指南

做网站怎么租个域名:保姆级建站教程避坑指南 还在为模板网站太丑、功能受限而头疼?想自己动手搞个高大上的官网,却卡在第一步不知道 做网站怎么租个域名 ?别急,这篇 保姆级建站教程 专治各种“域名小白”,从注册到部署,手把手教你把地基打牢,拒绝被忽悠。 域名不是租来的,是注册来的…

作者头像 李华
网站建设 2026/9/28 6:25:55

接做网站的私活图解步骤:3档预算避坑全解

接做网站的私活图解步骤:3档预算避坑全解 改个需求建站公司拖一周,这种憋屈事儿谁没碰过?我干这行十年,见过太多老板被外包坑得肉疼,要么功能缩水,要么后期维护费比建设费还高。想接 做网站的私活 ,或者自己把控项目,光靠嘴说没用,得看 图解步骤…

作者头像 李华
网站建设 2026/9/28 6:25:54

找seo优化培训机构别踩坑:3年老兵的避坑指南

找seo优化培训机构别踩坑:3年老兵的避坑指南 模板网站太丑不够用,改来改去还是差点意思,想自己搞点技术又不知道从哪下手?别急着报班,先看完这份避坑指南。很多人花了几万块去所谓的“seo优化培训机构”,结果回来发现教的都是十年前的老黄历,连基础的HTML5语义化标签都没讲清楚,更别提什么Core…

作者头像 李华
网站建设 2026/9/28 6:25:43

哈尔滨建站的网站网页选免费工具避坑实战指南

哈尔滨建站的网站网页选免费工具避坑实战指南 还在为找到的模板网站丑得掉渣、功能又不够用而头疼吗?很多哈尔滨本地的老板一上来就问“这网页怎么这么难看”,其实问题往往出在选错了起步工具。别急着花大钱定制,先试试那些靠谱的 免费工具 ,往往能解决80%的视觉和基础功能痛点,再谈深度定制不迟。…

作者头像 李华
网站建设 2026/9/28 6:25:38

正泰DDSU666 Modbus寄存器地址配置与映射表详解

1. 为什么正泰DDSU666的Modbus配置总让人栽跟头搞过配电监测或者能耗采集的人&#xff0c;对正泰DDSU666这款导轨式电能表应该不陌生。单相、体积小、带RS485接口、支持Modbus RTU&#xff0c;价格也友好&#xff0c;用在楼层配电箱、商铺分表、充电桩计量这些场景里非常合适。…

作者头像 李华