Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e2ca973
feat(pilot): 做掉 FU-1/FU-2/FU-4 —— flag 改白名单 + safe-cleanup 支持 squash 仓库
jhfnetboy Aug 5, 2026
8032203
docs: 账本 FU-1..FU-4 全部标记 done=PR#45
jhfnetboy Aug 5, 2026
6fc9502
fix(pilot): status 阶段接上 --squash-merged —— 否则修的东西到不了调用方
jhfnetboy Aug 5, 2026
7f9dd08
fix(pilot): 修评审四条 —— 证据收敛到 integration / 拒未知参数 / 区分查不了 / worktree
jhfnetboy Aug 5, 2026
0ff600d
merge main into feat/guards-squash-and-whitelist
jhfnetboy Aug 5, 2026
33cccf9
fix(pilot): jq 谓词改用 $ENV 传参 —— 分支名不能再改写判据
jhfnetboy Aug 5, 2026
bc840cf
fix(pilot): 第二轮评审 —— jq 注入 + protected 语义分歧 + 成本闸门 + 6 条 Low
jhfnetboy Aug 5, 2026
929d8d5
fix(pilot): 第三轮 —— tag 劫持 / -D 到不了调用方 / 7 条 Low
jhfnetboy Aug 5, 2026
7739473
fix(pilot): 第五轮 —— 两个 -D 站点合成一个 CAS 删除,不再各修一半
jhfnetboy Aug 5, 2026
47b7ac2
docs: 记 FU-9 —— --remote 在 squash 仓库里是死代码(第五轮先只让它说清楚)
jhfnetboy Aug 5, 2026
78c2735
merge main into feat/guards-squash-and-whitelist
jhfnetboy Aug 5, 2026
e4a93f3
merge main into feat/guards-squash-and-whitelist (#50 的 locale 修复)
jhfnetboy Aug 5, 2026
3dcbdd5
fix(pilot): 第六轮 —— 补上第五轮漏做的三条 + 服务端删除的新 High
jhfnetboy Aug 6, 2026
28c1df8
refactor(pilot): squash 清理改成只列不删 —— 砍掉全部不可逆操作
jhfnetboy Aug 6, 2026
638e224
fix(pilot): 第八轮 —— 清扫再跑一遍,这次针对文件系统写入
jhfnetboy Aug 6, 2026
16fd743
merge main into feat/guards-squash-and-whitelist (#49)
jhfnetboy Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions docs/agent/followups.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,26 @@
>
> **以 FU-4 的判据为准**:本地 tip **==** 或 **是任何一个**已合并 PR 的 `headRefOid` 的祖先
> (先 `git fetch origin pull/N/head` 把 head 抓到本地再算祖先)。这一条同时挡住上面两种错。
>
> **⚠️ 再更正(实现时找到更好的)**:FU-4 的祖先算法是对的,但**实现用的不是它**。
> GitHub 有 `GET /repos/{owner}/{repo}/commits/{sha}/pulls`,直接回答「哪个 PR 把这个 commit
> 引入了仓库」。它同样按 commit 判、两种错都没有,而且**每个分支只要一次 API 调用**、
> 不用把所有 PR head 抓到本地 —— dry-run 因此保持零写入。四种形状实测通过:
> squash 后的 head ✓、分支中间的 commit ✓、CLOSED 未合并 → 无证据 ✓、从未开过 PR → 无证据 ✓。
> 落地在 `safe-cleanup.sh` 的 `merged_pr_for()`。

- [x] FU-1 · B · src=PR#39 review [Low] · 2026-08-05 · git-guard merge-pr 拒绝 gh flag 用的是黑名单(--admin/--repo/-R 及其粘连形式)。黑名单追不上新 flag —— 以后 gh pr merge 若新增能绕过分支保护的 flag,这里不会自动知道。改成白名单(只放行 --squash/--merge/--rebase 等已知安全 flag)更耐久 · done=PR#47
- [ ] FU-2 · B · src=2026-08-05 合并 #38/#39/#40/#41 后实测 · 2026-08-05 · safe-cleanup.sh 在 squash-merge 仓库里永远清不掉任何分支:本仓库 28 个本地分支,git branch --merged main 返回 0 个,因为 squash 后原 commit 不是 main 的祖先,而 safe-cleanup 只用 -d 永不 -D。这是继 #39(死代码)、#40(随机红灯)之后同一家族的第三个『守卫跑不起来』。正确改法:用 gh 核实『存在 headRefName==该分支且 state==MERGED 的 PR』作为已合并证据,再允许 -D;不能简单放开 -D
- [ ] FU-3 · C · src=PR#42 review [Low] · 2026-08-05 · check-version-sync.sh 只比对 plugin.json 与 SKILL.md 两处。今天 README 没有硬编码版本号(核过),所以没问题;但哪天 README 加上版本,这条守卫不会知道。在脚本里写一句把范围钉住:『目前只有这两处声明版本』
- [ ] FU-4 · B · src=PR#42 review [Low] + 2026-08-05 清理 28 个分支的实测 · 2026-08-05 · 补充 FU-2 的实现要点(今天手工做过一遍,算法已验证):① git branch --merged 和 git cherry 在 squash 仓库里【全部失效】—— cherry 对 12 个分支全报『未在 main』,因为 squash 重写补丁、patch-id 永不匹配;② 可用判据是『本地 tip == 或 是 任何一个已合并 PR 的 headRefOid 的祖先』,要先 git fetch origin pull/N/head 把 head 抓到本地;③ 【不能只按分支名匹配 PR】—— work-pr18 / fix-pr18-round2 / worktree-agent-* 这三个分支名从没当过 PR head,但 tip 就是 PR#18/#23 的已合并 head,按名字匹配会漏掉;④ 反向风险(评审提的):分支名可复用,同名分支删掉重开后内容不同,旧 MERGED PR 仍在 —— 祖先检查恰好挡住这种情况(重开的 tip 不会是旧 head 的祖先),但若改成只按名字匹配就会误删
- [x] FU-2 · B · src=2026-08-05 合并 #38/#39/#40/#41 后实测 · 2026-08-05 · safe-cleanup.sh 在 squash-merge 仓库里永远清不掉任何分支:本仓库 28 个本地分支,git branch --merged main 返回 0 个,因为 squash 后原 commit 不是 main 的祖先,而 safe-cleanup 只用 -d 永不 -D。这是继 #39(死代码)、#40(随机红灯)之后同一家族的第三个『守卫跑不起来』。正确改法:用 gh 核实『存在 headRefName==该分支且 state==MERGED 的 PR』作为已合并证据,再允许 -D;不能简单放开 -D · done=PR#45
- [x] FU-3 · C · src=PR#42 review [Low] · 2026-08-05 · check-version-sync.sh 只比对 plugin.json 与 SKILL.md 两处。今天 README 没有硬编码版本号(核过),所以没问题;但哪天 README 加上版本,这条守卫不会知道。在脚本里写一句把范围钉住:『目前只有这两处声明版本』 · done=PR#43
- [x] FU-4 · B · src=PR#42 review [Low] + 2026-08-05 清理 28 个分支的实测 · 2026-08-05 · 补充 FU-2 的实现要点(今天手工做过一遍,算法已验证):① git branch --merged 和 git cherry 在 squash 仓库里【全部失效】—— cherry 对 12 个分支全报『未在 main』,因为 squash 重写补丁、patch-id 永不匹配;② 可用判据是『本地 tip == 或 是 任何一个已合并 PR 的 headRefOid 的祖先』,要先 git fetch origin pull/N/head 把 head 抓到本地;③ 【不能只按分支名匹配 PR】—— work-pr18 / fix-pr18-round2 / worktree-agent-* 这三个分支名从没当过 PR head,但 tip 就是 PR#18/#23 的已合并 head,按名字匹配会漏掉;④ 反向风险(评审提的):分支名可复用,同名分支删掉重开后内容不同,旧 MERGED PR 仍在 —— 祖先检查恰好挡住这种情况(重开的 tip 不会是旧 head 的祖先),但若改成只按名字匹配就会误删 · done=PR#45
- [ ] FU-5 · B · src=2026-08-05 pilot 端到端测试(doctor+plan) · 2026-08-05 · pilot 的起跑门禁只认 docs_dir 下七个固定文件名,认不出等价(且更完整)的规划源。实测:Brood 的规划在 backlog/(4 个 milestone + 49 个带验收标准的 task + 2 个 ADR),check-docs.sh --strict 报 0/7、run 直接 fail-closed 拒跑;而 plan.md A.3 又明写『已有规划 → 不要重复造』—— 两条同时遵守不可能。本次用 docs/agent/ 做适配层(指向 backlog/ 的视图,不复制内容)绕过去了,但根治要给 check-docs.sh 加可配置规划源(如 .pilot.yml 声明 planning_source: backlog),否则每个用 backlog/issues/Jira 管规划的仓库都会被判未就绪
- [x] FU-6 · C · src=2026-08-05 pilot 端到端测试(doctor) · 2026-08-05 · doctor 第 4 步在单主干仓库里会误导:无 .pilot.yml 时默认 integration_branch=preview,doctor 发现它不存在就『提示先建』—— 但对单主干仓库正确答案是 integration_branch=main + 合并时用 --allow-trunk,不是去建一个 preview 分支。doctor 不知道这两件事是连着的。改法:检测到 preview 不存在但 default branch 存在时,提示单主干配置法并指向 --allow-trunk · done=PR#48
- [ ] FU-7 · B · src=2026-08-05 从 PR#45 拆出 PR#47 时实测 · 2026-08-05 · 『dist/ matches a fresh build』这条 CI 检查会随日期自己变红,与代码无关。实测:在 main(5549b8b)上不改任何代码只跑一次 preflight,dist/api/statistics.json 就变了 —— averageTaskAge 98→99,那是按天算的任务平均年龄。也就是说 main 放几天不动,下一个 PR 无论改什么都要顺带提交一次无关的 dist 变更,否则红灯。这是继 #39(死代码)、#40(随机红灯)、FU-2(守卫清不掉东西)之后同一家族的第四个『守卫自己不可靠』。改法二选一:① 比对时把时间派生字段(averageTaskAge、recentActivity 里的相对时间)剔除再 diff;② 导出时就不把这类字段写进 dist。⚠️ pilot 冻结期内不做,记账待解冻
- [x] FU-7 **撤回(误报)** · 2026-08-05 同日核实 · append-only 不删行,所以更正写在这里:**这条不成立,不要去修。** verify.yml 第 96–122 行**已经**在比对前把 `averageTaskAge` pin 到 `git show HEAD:` 里的 committed 值(正是 PR#40『归一化那一个字段』做的,注释里连 04:15 UTC 测到 98、十五分钟后 99 都记了),所以 CI 不会因它变红。我立 FU-7 时只看到本地 `git status` 有 diff 就下了结论,没读 CI 脚本 —— **本地 dist 会漂 ≠ CI 会红**,这两件事被我混成一件。真实结论:本地跑完 build 看到 statistics.json 变了,**不需要**提交,CI 会自己 pin 掉。教训比这条 bug 本身有用:报「守卫不可靠」之前先读那个守卫的实现,别只看症状 · done=PR#47(撤回)
- [ ] FU-8 · B · src=PR#47 review R3(Codex)+R4 建议 · 2026-08-05 · **每个守卫必须校验自己的选择器,而不只是 flag** —— 这条纪律要写进 reference/。来源:PR#47 的白名单把 gh flag 管得很严(不认识就拒),却从没看过 merge-pr 的第一个位置参数,而 `gh pr merge` 接受 `[<number>|<url>|<branch>]`,URL 选择器完全无视 `--repo`(gh 2.92.0 实测),于是所有闸门读本仓库、合并落到另一个仓库。已在 PR#47 修掉那一处实例(选择器必须是纯数字),但**纪律本身没落地**,下一个守卫照样可能只查 flag。这是本仓库同一家族的第五个:#39 死代码 / #40 随机红灯 / FU-2 清不掉东西 / FU-7(我自己的误报) / 本条 —— 共同形状是『读起来很严、但有一个输入它从来不检查』。落地时和 #45 一起收口,不要再往 PR#47 里加东西(它被拆出来就是因为 #45 装了四件事)
- [ ] FU-9 · B · src=PR#45 review R4 [Low] · 2026-08-05 · safe-cleanup 的 §3(--remote)只用 `git branch -r --merged` 找候选,而那个判据在 squash 仓库里按构造恒返回 0 行 —— 也就是说在【这个功能存在的理由所指的那种仓库形态里,第 3 节是死代码】,而 run.md 又把 --remote 写成合并后的远程清理手段。--squash-merged 没有延伸到远程。第五轮先只让它把话说清楚(为空时打印『没查,不是没有』,并指向手工清理 / GitHub auto-delete),没有实现远程的证据检查 —— 那要对每个远程分支再花一次 gh 调用,且远程路径没有 -d 兜底,风险高于本地,值得单独一个 PR 想清楚。这是同一家族的第六个『守卫在它最该起作用的场景里跑不起来』(#39 死代码 / #40 随机红灯 / FU-2 清不掉 / FU-7 我的误报 / FU-8 只查 flag 不查选择器 / 本条)
- [x] FU-2/FU-4 **收敛(2026-08-06)** · append-only 不删行,所以改法写在这里:**squash 清理最终只『列』不『删』**。PR#45 在这个能力上走了六轮评审,每轮都挖出实测复现的真缺陷(同名 tag 劫持→删掉未合并分支 / 恢复句柄打印另一个分支名(bash 3.2 的 local 语义) / 丢掉 git 自带的 worktree 占用拒绝 / TOCTOU / 拿本地判据在服务端删掉同事未合并的工作)。没有一条是评审吹毛求疵。**结论不是防得更严,而是:自动执行不可逆删除、判据又必须从服务端推断,所需的把握程度配不上它买到的东西 —— 它买到的只是不用敲 `git branch -D <名字>`。那六个缺陷全是「删」的属性,不是「列」的属性。** 所以脚本做难的那半(逐条给出合并证据),不可逆的那半留给人;远程分支交给 GitHub auto-delete-on-merge。FU-9(远程在 squash 仓库里是死代码)一并作废 —— 那一节现在也只列不删 · done=PR#45
- [ ] FU-10 · C · src=PR#45 第八轮 [Low] · 2026-08-06 · **注释与文档漂移(收口时未清)**:safe-cleanup.sh 里若干注释仍在引用已删掉的东西 —— :150-151 / :247 / :317 提到 `update-ref` 的 expected-old-value(那个函数已删)、:501 附近说 §3「deletes on the SERVER」(已改成完全不处理)。另外 run.md:76 写「不加 --delete-branch,远程分支删除统一交给 safe-cleanup」,但 safe-cleanup 已不处理远程、git-guard 又硬拒 --delete-branch,于是**远程 head 分支的清理在文档流程里没有主人**,只剩 GitHub 的 auto-delete-on-merge,而 skill 既不检查该设置是否打开、也没在任何地方教人去开。改法:清一遍过时注释;在 doctor 里加一条只读检查「本仓库是否开了 auto-delete-on-merge」并在关闭时提示
- [ ] FU-11 · C · src=PR#45 第八轮 [Low] · 2026-08-06 · §1b 现在是个纯报告,但计价没变:每个候选分支一次 gh API 调用(本仓库约 25 次 / 20.6 秒),而 status.md 让它在**每次** `pilot status` 都跑。改法二选一:① 按 tip sha 做本地缓存(tip 没动就不重查);② 在 status.md 里把 `--squash-merged` 改成显式 opt-in,默认不带
- [ ] FU-12 · C · src=PR#45 第八轮 [Low] · 2026-08-06 · §1b 会把「某个活着的 worktree 正在 rebase/bisect 的分支」当成游离分支列出来并打印 `git branch -D <b>`,而同一次运行里 §2 对那个 worktree 打的是 KEEP —— **同一份输出自相矛盾**。已验证无害(git 2.50 会拒绝 `cannot delete branch 'x' used by worktree`,分支完好,rebase --continue 正常),所以只是输出问题。顺带记一句:这也说明删掉 `ref_in_use_by_worktree` 是对的 —— `git branch -D` 自带那个拒绝,而 `update-ref -d` 没有

3 changes: 2 additions & 1 deletion plugins/pilot/skills/pilot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ pilot doctor # 自检本地就绪度(config/docs/分支/gh/hook)——**
## 为什么用它
把「有经验程序员的默认」固化成流程,且**危险动作确定性可控**:

- **安全清理**:只删「已合并进集成分支 + 干净」的分支/worktree;只 `git branch -d`(永不 `-D`);默认 dry-run,`--apply` 才动手;护住主干/集成/当前/protected/脏 worktree。逻辑全在 `scripts/safe-cleanup.sh`,不靠模型临场判断。
- **安全清理**:**唯一会执行的删除是 `git branch -d`**(git 自己拒绝未合并的、以及被 worktree 占用的)。**永不 `-D`、永不删远程、永不 `git worktree remove`**——最后这条是**文件系统删除**,会连 `.gitignore` 掉的文件(真实案例:`.env`)一起抹掉,而 `git status --porcelain` 根本不显示它们,所以 worktree 只**列出来**并给出命令。远程分支**完全不处理**,用 GitHub 的 auto-delete-on-merge。逻辑全在 `scripts/safe-cleanup.sh`,不靠模型临场判断。
**squash-merge 仓库**里 `git branch --merged` 通常返回 0(squash 重写补丁,原 commit 不是集成分支的祖先),于是上面那条 `-d` 在这类仓库里清不动任何东西。加 `--squash-merged`:它按 **commit** 向 GitHub 查「哪个已合并 PR 引入了这个提交」,把有证据的分支**列出来**(附 PR 号 + tip sha + 可粘贴的 `git branch -D` 命令)——**脚本自己不执行那一下**。难的那半(在 git 看不出来的仓库里逐条给出证据)自动做,不可逆的那半留给人。查不到证据、没装 gh、没登录 —— 一律不列,并明说「无法核实」。
- **PR 纪律**:绝不 `git add -A`;绝不直推/直合主干;一个 task = 一个分支 = 一个 worktree = 一个 PR;PR 前必自测 + 对抗式 review。
- **外部 review 回路(已生产验证)**:pilot **不自评 PR**——开好 PR 后只盯自己 PR 的状态(`scripts/pr-monitor.sh --pr <n> --wait-for-verdict`,内置 3–5 分钟轮询与 **30 分钟硬上限**;只有评审 commit == 当前 head 才算裁决)。裁决由**外部评审服务**给出,契约见 `reference/review-contract.md`:排队 5–10 分钟、评审 5–10 分钟,通常 20 分钟内出 `APPROVED`/`CHANGES_REQUESTED`(超大 PR 例外)。推新 commit 自动触发再评审。**那个服务是什么、装在哪、覆盖哪些仓库,pilot 一概不知也不启动**——只依赖这份契约。
- **pilot 是入口,配套能力由它安排**:飞书 / Notion 等文档源是 pilot 的**配套 skill**——该不该装、装哪个、装到全局还是项目级、装完怎么验证,由 pilot 负责讲清楚和安排。**但「入口」是编排责任,不是运行时依赖**:pilot 自己不 import、不启动任何文档源,只探测能力是否存在,没有就说明缺什么、给出装法,然后降级继续干活。契约与实测过的安装命令见 `reference/doc-sources.md`。
Expand Down
2 changes: 1 addition & 1 deletion plugins/pilot/skills/pilot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pilot doctor # 自检本地就绪度(config/docs/分支/gh/hook)——**
1. **绝不 `git add -A` / `git add .`**。只 `git-guard.sh add <显式路径>`(裸 `git add -A` 会被 git-guard 拒绝)。理由:`-A` 会把未确认是否该跟踪的文件(密钥、`.env`、构建产物、临时文件)一起提交,是最危险的日常动作。提交前先 `git status` 看清,逐一列出要提交的路径。
2. **绝不直接 push 到主干(main/master),绝不直接合并自己的 PR 到主干**。push 走 `git-guard.sh push`、**开 PR 走 `git-guard.sh pr-create`**(先 `preflight.sh run` 让本仓库的检查真跑过)、合并走 `git-guard.sh merge-pr --integration <b>`(推主干 / 合并 base≠集成分支都会被硬拒绝)。所有代码变更走:feature 分支 → PR → review → 合并到**集成分支**(默认 `preview`,见 `.pilot.yml`)。主干只由集成分支经受控流程进入。**单主干仓库**(没有集成分支,PR 直接开向 `main`)加 `--allow-trunk`——它**不是绕过**:仍要求该分支的 GitHub 保护规则要求审批、且这个 PR 已经 `APPROVED`,读不到保护规则就拒绝(fail-closed)。
3. **一个 task = 一个分支 = 一个(可选)worktree = 一个 PR**。不在一个分支里顺手做别的 task。
4. **删除分支只用 `git branch -d`,永不 `-D`**;删除只针对「已合并 + 干净」的分支/worktree;一切经 `scripts/safe-cleanup.sh`,默认 dry-run。
4. **绝不 `git branch -D`,绝不 `git push --delete`,绝不 `git worktree remove`**(最后这条是文件系统删除,会连 gitignore 掉的文件一起抹掉,而 `git status --porcelain` 看不见它们)。脚本唯一会执行的删除是 `git branch -d`——git 自己会拒绝未合并的、以及被 worktree 占用的,这是安全网。worktree 和远程分支只**列出来**给人。一切经 `scripts/safe-cleanup.sh`,默认 dry-run。**squash-merge 仓库**里 `git branch --merged` 通常为空,`--squash-merged` 会按 commit 向 GitHub 核实哪个已合并 PR 引入了它,**但只把结果列出来给人,脚本不执行 `-D`**——不可逆的那一下由人来敲(详见 `reference/git-safety.md`)。
5. **PR 之前必须自审 + 对抗 review**(怎么审见 `reference/pr-quality.md`,**审几轮由 `scripts/grade-change.sh` 机械定级,见 `reference/pre-pr-review.md`**——A/B 级 3 轮,不是作者自己说了算)。没过 review 的代码不进 PR,没 approve 的 PR 不合并。
6. **状态即文档**。每推进一步都更新 `docs/agent/tasks.md` 与 `docs/agent/progress.md`;宁可慢,不可让文档与仓库真实状态脱节。
7. **无人值守时不猜产品决策**。遇到影响产品方向/验收/架构的未知,把相关 task 标 `BLOCKED` 并记录待决问题,继续做不受影响的 task;绝不擅自替用户拍板。
Expand Down
Loading
Loading