fix(pilot): 收尾 —— 两条会让人粘贴出坏结果的命令 + 清死配置 - #52
Conversation
PR#45 第九轮 APPROVE 时附带的建议。挑出【会让人照着粘贴、然后得到坏 结果】的两条修掉,其余记账。 ## 1. §2 对 squash 分支打印 -d,而 squash 后 -d 必然失败 §1b 对同一类证据打的是 -D,§2 一律打 -d。squash 之后原 tip 不是集成 分支的祖先,所以那条 -d 【不可能成功】。实测照着粘贴: git worktree remove <path> → 成功,目录没了 git branch -d sqfeat → error: the branch 'sqfeat' is not fully merged → 目录已删、分支还在 = 孤儿分支 而在 squash 仓库里这是【每一条】—— 也就是这个功能唯一存在理由的那种 仓库形态。改成和 §1b 一致:via=git 打 -d,squash 证据打 -D。 ## 2. main_root 取的是「你站在哪个 worktree」,不是主 worktree --show-toplevel 在 linked worktree 里返回那个 worktree 自己。而按本 skill 自己的教条(一个 task = 一个 worktree),「从 linked worktree 里跑」 正是常态,status.md 还让它每次 pilot status 都跑。 后果:§2 把当前 worktree 整个漏掉,反而打印「删掉那个装着真 .git 的 主仓库目录」的命令。目前无害(git 两半都拒绝),但那是一条递给人粘贴的 错命令;而在上一个 commit —— §2 还会真执行 remove 的时候 —— 它是一次 对主 checkout 的删除尝试。 改用 git worktree list --porcelain 的第一段(永远是主 worktree)。 ## 3. 清掉 allow_remote_cleanup 这个死配置 grep 确认【没有任何代码读它】,而 status.md/SKILL.md/README/模板 还在 把它当作生效的闸门、指挥模型去用一个已经不存在的能力。四处清掉,改成 指向 GitHub 的 auto-delete-on-merge。 (run.md 里那处留给 PR#51 —— 它正在改同一个文件,避免冲突) ## 记账不做 FU-13:--remote/--remote-name 已成摆设(仍被解析、仍在 --help 里,但只 控制一段从文档流程里已不可达的说明文字)。 FU-14:--integration 只决定候选列表,真正执行的 git branch -d 判的是 「合进 HEAD」—— 站在非 integration 分支上会如实转达一个假的「未合并」。 Claude-Session: https://claude.ai/code/session_01CmAW1q62bBtjT99inZeyLk
clestons
left a comment
There was a problem hiding this comment.
✅ APPROVE — [4-round]
两条修复都是真的,我从零重验过;余下四条 Low 建议下一个 commit 收掉,不阻塞。
两条修复实证
L2 main_root —— 从 linked worktree 里跑:
parent: /private/tmp/l3/repo [primaryfeat] (git)
remove with: git worktree remove /private/tmp/l3/repo && git branch -d primaryfeat
← 递给人一条「删掉主 checkout」的命令,同时把它站着的那个 worktree 整个漏掉
HEAD: 正确跳过主 worktree,评估的是自己
六种拓扑全验过:主 worktree、主里的子目录、linked、linked 里的子目录、bare 仓库的 linked worktree(HEAD 跳过 bare 目录,parent 把它列成候选)、bare 目录本身。符号链接 checkout 下 --show-toplevel 与 worktree list --porcelain 给出的路径形式一致,所以 :419 没引入路径形态变化。
L1 -d → -D —— squash 仓库里逐字粘贴:
parent 的 -d: git worktree remove 成功 → error: the branch 'sqfeat' is not fully merged
→ 目录没了、分支还在 = 孤儿分支
HEAD 的 -D: Deleted branch sqfeat (was a38d194),exit 0
-D 在这里是对的,虽然它抽象上更狠:squash 之后原 tip 按构造就不是祖先,-d 不可能成功;§1b 对同一类证据本来就打 -D;而且这是打印给人看的,不是脚本执行的。
L3 —— grep 确认没有任何代码读 allow_remote_cleanup,前提成立。git merge-tree --write-tree origin/main fdda4254 rc=0 无冲突;PR#51 已经合了(main tip 1a91dc4)并且已经把 run.md:77 那句删掉了,所以「留给 #51」这个延期已经兑现。via 在所有能走到 printf 的路径上都被赋值(其余路径全 return)。bash -n 干净。
后续项(四条 Low,都不阻塞)
1. :419 —— 本次新引入的一条中断路径(parent 没有)
main_root="$(git worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p' | head -1)"
[ -n "$main_root" ] || main_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"这是管道,而文件在 :66 开了 set -euo pipefail(:431/:434 那对 set +e/-e 在 handle_wt 里,位置更靠后)。管道非零 → 赋值非零 → 就地终止在 :419,下一行的 || 兜底根本到不了。
对着真脚本复现,两种触发都完全静默:
old-git shim(worktree list 退 129) → HEAD exit=129,parent exit=0 且报告完整
SIGPIPE(head -1 提前关管道) → HEAD exit=141,parent exit=0
两者 stderr 均 0 字节,输出恰好截断在 "## Worktrees (clean + merged only)" 这一行
这正是同一个文件在 :252-256 亲自记录过的坑(list_has 之所以不用管道就是为了躲它),而 :521 那个兄弟 reader 用的是进程替换、免疫。
为什么只给 Low:实测阈值 300 个 worktree 还是 exit 0、500 个才 141(约 20KB sed 输出),--porcelain 从 git 2.7(2016)就有;丢的内容是纯报告,而且非零退出码 + 缺失的 == done == 尾行在调用点都是可见的信号。改法:像 :521 那样用进程替换,或者加 || true。
⚠️ 一个需要留意的组合:在--apply下,§1 的git branch -d删除已经发生了脚本才死;而status.md要求把输出原样转达,于是模型会把一次被截断的运行报告成完整的。
2. :508 —— L1 只修了 if 的一条臂,另一条臂带着同一个 bug(但这是旧问题,不是回归)
git branch -d 判的是粘贴时那个 shell 的 HEAD,不是 --integration。从 linked worktree 里逐字复现:git worktree remove 成功 → error: the branch 'gitfeat' is not fully merged → 目录没了、分支成孤儿,和 L1 要消灭的半成品粘贴一模一样。而本 PR 的 L2 修复恰恰是因为「站在 linked worktree 里」是文档里的常态——这个 PR 的两半在这一点上互相矛盾。
不阻塞的理由:parent 对每一行都打 -d,本 PR 修掉的正是 squash 仓库里占 100% 的那一条臂,爆炸半径是被缩小了而不是扩大。另外 FU-14 覆盖不到这条:FU-14 记的是 §1 里 fail-safe 的漏删,而这里 && 把它和一个先执行的、不可逆的文件系统删除绑在了一起。
3. :510 —— 采纳了 §1b 的 -D,没采纳 §1b 的披露
§1b 会打 tip=%s,收尾还有「── listed, NOT deleted. Read the PR numbers above」;§2 这行两样都没有——恢复凭据恰好缺在有 force-delete 的那一处。已演示的陈旧报告窗口:打完报告后往那个干净 worktree 里提交新工作,再粘贴 → worktree remove 成功(新工作已提交所以树是干净的),branch -D 把指向不可达 commit 的指针丢掉;parent 的 -d 在同一场景下会拒绝。这比 R1a 建议的「加一句 force-delete 警告」更对症。
4. .pilot.yml:11 —— 仓库自己的配置仍写着 allow_remote_cleanup: false,而本 PR 改的四份文档都已宣布这个键是死的。合并后核实过,这是仅剩的一处。
建议
:419改成done < <(...)式的进程替换(和:521一致)或加|| true,既恢复:420的兜底,也重新遵守:252-256自己立的规矩。:507建议反过来写:case "$via" in squash-*) -D ;; *) -d ;; esac。现在else那一臂让 force-delete 成了默认,将来任何一条via未设置就走到 printf 的路径会静默拿到-D。:510补tip=%s和 §1b 的收尾句;FU-14 扩一条记 §2 变体(不可逆的文件系统删除先执行,所以「fail-safe 方向」在这里不成立)。
驳回
- R1a F1 的机理说错了(结论方向对):它说「兜底只覆盖空输出、不覆盖非零退出,且是 old-git 特有」。两处都不对——
$( )只捕获 stdout,所以空 stdout 本来是能走到兜底的;兜底之所以是死代码,是set -e先在:419终止了。而且不是 old-git 特有,当前 git 2.50.1 上 SIGPIPE 一样触发。 - R4 自己的一个假设也被证伪并记录在此:怀疑 BSD
sed遇到非 UTF-8 字节序列会失败(那会让:419好触发得多)——APFS 根本不允许创建这种路径的 worktree(fatal: could not create directory … Illegal byte sequence),输入不可能存在。
一句话说明为什么放行
一条实践中够不到的回归(要 ~500 个 worktree 或 2016 年的 git,落在纯报告路径上,还有两个可见信号)+ 一条被这个 PR 缩小了爆炸半径的旧问题,对上两条已验证的真修复——而且修的是这个 skill 在每次 pilot status 都递给人粘贴的命令。这是明确的净改进,够不上阻塞线;四条 Low 属于下一个 commit 或跟进账本。
4-round:R1a/R1b DeepSeek-v4-flash(F1 落点对但机理说错、已驳回其推理;F2 成立但有更对症的替代;R1b 无 finding)→ R2 Opus 独立评审(自建可用的 gh stub 打到 squash 分支、逐字粘贴命令、六种 worktree 拓扑,纠正 F1 机理并补出 :419/:508/:510/.pilot.yml:11)→ R3 Codex 对抗挑战(独立沙箱,0 挑战 / 2 确认:E1 静默退 129/141、E2 孤儿分支)→ R4 Opus 终裁 + 全量复扫(四条全部独立复现,实测出 300/500 worktree 的触发阈值从而把 :419 定为 Low,确认 :508 是 parent 既有行为而非回归,并证伪了自己关于非 UTF-8 路径的假设)。
PR#45 第九轮 APPROVE 时附带的建议。挑出会让人照着粘贴、然后得到坏结果的两条修掉,其余记账。
1. §2 对 squash 分支打印
-d,而 squash 后-d必然失败§1b 对同一类证据打的是
-D,§2 一律打-d。squash 之后原 tip 不是集成分支的祖先,那条-d不可能成功。实测照着粘贴:对照(同一个 fixture):
而在 squash 仓库里这是每一条 —— 也就是这个功能唯一存在理由的那种仓库形态。
2.
main_root取的是「你站在哪个 worktree」,不是主 worktree--show-toplevel在 linked worktree 里返回那个 worktree 自己。而按本 skill 自己的教条(一个 task = 一个 worktree),「从 linked worktree 里跑」正是常态,status.md还让它每次pilot status都跑。后果:§2 把当前 worktree 整个漏掉,反而打印「删掉那个装着真
.git的主仓库目录」的命令。目前无害(git 两半都拒绝:fatal: '…' is a main working tree/cannot delete branch … used by worktree),但那是一条递给人粘贴的错命令;而在上一个 commit —— §2 还会真执行git worktree remove的时候 —— 它是一次对主 checkout 的删除尝试。改用
git worktree list --porcelain的第一段(永远是主 worktree),保留--show-toplevel作兜底。实测:从 linked worktree 里跑,现在正确报告自己,不再把主仓库列为候选。
3. 清掉
allow_remote_cleanup这个死配置grep确认没有任何代码读它(#45 移除远程处理之后),而status.md/SKILL.md/README.md/templates/pilot.example.yml还在把它当作生效的闸门 —— 也就是在主动指挥模型去用一个已经不存在的能力。四处清掉,改成指向 GitHub 的 auto-delete-on-merge。run.md里那处留给 PR#51 —— 它正在改同一个文件,避免冲突。记账不做
--remote/--remote-name已成摆设:仍被解析、仍在--help的 Usage 里,但只控制一段从文档流程里已经不可达的说明文字(status.md/run.md 都不再传--remote)--integration X只决定候选列表,真正执行的git branch -d判的是「合进 HEAD」。站在非 integration 分支上,每个真正已合并的候选都会打印SKIP (not safely merged)—— 方向 fail-safe(只漏删不误删),但status.md要求把输出原样转达,于是会如实转达一个假的「未合并」preflight.sh run4/4 通过。https://claude.ai/code/session_01CmAW1q62bBtjT99inZeyLk