Skip to content

feat(pilot): FU-1 —— git-guard merge-pr 的 gh flag 改白名单(从 #45 拆出) - #47

Merged
jhfnetboy merged 7 commits into
mainfrom
feat/guard-allowlist
Aug 5, 2026
Merged

feat(pilot): FU-1 —— git-guard merge-pr 的 gh flag 改白名单(从 #45 拆出)#47
jhfnetboy merged 7 commits into
mainfrom
feat/guard-allowlist

Conversation

@jhfnetboy

@jhfnetboy jhfnetboy commented Aug 5, 2026

Copy link
Copy Markdown
Member

为什么拆

PR #45 一次装了四件事(白名单 + squash 清理 + worktree + 成本闸门),已经走到第四轮评审,
每轮都能挖出真问题,但交互面也一轮比一轮大。这里把三轮评审从没点过名的那部分单独切出来先合,
#45 之后只剩 squash 清理一件事。

这个 PR 只做一件事

git-guard.sh merge-pr 拒绝 gh flag 的方式,黑名单 → 白名单。

黑名单只能拒绝今天已知的危险 flag。gh pr merge 将来新增一个能绕过分支保护的 flag,
黑名单会静默放行,这里不会自动知道。白名单往相反方向失败:不认识就拒,要放行得有人明确加进去。

  • 放行:--squash --merge --rebase --auto,以及 --body/-b --body-file/-F --subject/-t --match-head-commit 的分离与粘连两种形式
  • --admin / --repo / -R / --delete-branch 保留具名拒绝,只为让报错自己解释清楚(白名单本来也会拒)
  • 多余位置参数一并拒绝:PR 号是唯一位置参数
  • 顺带修一处注释:gh api 的错误 BODY 走 stdout,不需要 2>&1 —— 原注释说反了,照着改会把 stderr 混进 JSON 让解析全线降级

实测

本 PR 自己的 push 就走的 git-guard.sh push,PR 也是 git-guard.sh pr-create 开的,
preflight.sh run 4/4 通过(grade A)。第一次 pr-create 还被自己的 stale-stamp 守卫拦了一次,守卫工作正常。

账本里一条自己撤回的记录

开这个 PR 时我先立了 FU-7(『dist fresh-build 检查随日期变红』),同日核实后撤回 —— 那是误报
verify.yml 96–122 行早就在 diff 前把 averageTaskAge pin 到 committed 值(PR#40 做的),CI 不会因它变红。
当时只看到本地 git status 有 diff 就下了结论,没读 CI 脚本,把「本地 dist 会漂」和「CI 会红」混成了一件事。
对应那个多余的 dist commit 也一并 revert 了。撤回过程按 append-only 纪律留在 followups.md 里,没删行。

Closes FU-1

https://claude.ai/code/session_01CmAW1q62bBtjT99inZeyLk

黑名单只能拒绝今天已知的危险 flag。gh pr merge 将来新增一个能绕过
分支保护的 flag,黑名单会静默放行、这里不会自动知道。改成白名单:
不认识的 flag 一律拒绝,要放行得有人明确加进去并写明理由 —— 这是
守卫应该失败的方向。

- 放行:--squash/--merge/--rebase/--auto,以及 --body/-b、--body-file/-F、
  --subject/-t、--match-head-commit 的分离与粘连两种形式
- --admin/--repo/-R/--delete-branch 保留具名拒绝,只为让报错自己解释清楚
  (白名单本来也会拒)
- 多余位置参数一并拒绝:PR 号是唯一位置参数
- 顺带修一处注释:gh api 的错误 BODY 走 stdout,不需要 2>&1 —— 原注释
  说反了,照着改会把 stderr 混进 JSON 让解析全线降级

从 PR#45 拆出。#45 那边只留 squash 清理一件事。

Claude-Session: https://claude.ai/code/session_01CmAW1q62bBtjT99inZeyLk
fresh-build 检查会随日期漂移:statistics.json 的 averageTaskAge 按天算,
今天重建就与 main 里的不同。与本 PR 代码无关,但不提交 CI 会红。
这条漂移本身是个 followup,见 FU-7。

Claude-Session: https://claude.ai/code/session_01CmAW1q62bBtjT99inZeyLk
@jhfnetboy
jhfnetboy requested a review from clestons as a code owner August 5, 2026 15:34

@clestons clestons left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ REQUEST_CHANGES — [4-round]

黑名单→白名单这个方向是对的,而且严格比它替换掉的东西更严:被删掉的每个黑名单条目仍然各自具名 case 着,后面还有 -* 兜底;多余位置参数从"直接透传"变成了拒绝;所有拒绝都在解析阶段触发,早于 command -v gh,不需要网络和认证。实测拒绝(rc=3):--admin--author-email-s-m--disable-auto-bfoo、尾随位置参数、--body -R other/repo-R 被当成 body 值吃掉,other/repo 再作为位置参数被拒)、merge-pr --squash 47

这道白名单管住了 flag,没管住它自己的第一个位置参数,而那个参数能做到 --repo 被拒绝时想做的事。


🔴 Blocking

[High] git-guard.sh:156n="${1:-}" 从不校验,而 gh pr merge 接受 [<number> | <url> | <branch>];URL 选择器会覆盖被钉住的 --repo

实测链条(全部只读验证,gh 2.92.0):

$ gh pr view https://github.com/jhfnetboy/NextStop/pull/26 --repo AAStarCommunity/Brood --json number,baseRefName,url
{"baseRefName":"main","number":26,"url":"https://github.com/jhfnetboy/NextStop/pull/26"}

--repo 被 URL 完全无视。再在 Brood 检出目录里直接跑本 PR head 的守卫:

$ bash git-guard.sh merge-pr "https://github.com/jhfnetboy/NextStop/pull/26" --integration zzz-nonexistent-branch
git-guard: BLOCKED: PR #https://github.com/jhfnetboy/NextStop/pull/26 base is 'main', not integration 'zzz-nonexistent-branch'

守卫在 Brood 里读到了 NextStop 的 PR。把 integration 换成真实值走一遍单主干路径:

步骤 用的是哪个仓库 结果
repo="$(gh repo view …)" 本地 Brood(完全不看 $n AAStarCommunity/Brood
trunk 判定 / gh api repos/$repo/branches/$integration/protection 本地 Brood Brood main 的保护规则 → 通过
gh pr view "$n" --repo "$repo" 读 reviewDecision URL 指向的外部仓库 外部 PR 的 APPROVED → 通过
base / head 检查 外部仓库 通过
exec gh pr merge "$n" --repo "$repo" URL 指向的外部仓库 合的是外部 PR

一道闸门都拦不住。Brood 的分支保护被当成证据,去合另一个仓库的 PR —— 这正是第 186 行拒绝 --repo/-R("it would bypass the safety rail")要防的事。而且本 PR 新加的那条位置参数报错信息自己写着"the PR number is the first argument",却从来没检查过它是不是个数字。

来源上确实是既有问题(n="${1:-}" 不是这个 PR 引入的),但本 PR 的全部立意就是"控制什么东西能到达 gh pr merge",这道白名单声称关掉的正是这一类;放在这里修最合适。

修复:在第 197 行那组检查旁边加一行——

case "$n" in ''|*[!0-9]*) die "refusing PR selector '$n' on merge-pr — pass a bare PR NUMBER; a URL or branch name overrides the pinned --repo and would target a different repository" ;; esac

这一行顺带把下面 MISSED 里两条误导性报错也一并修了。


已确认

[Medium] git-guard.sh:178,234--auto 进了白名单,但 --allow-trunk 的证明只是调用瞬间的快照,且从不检查 dismiss_stale_reviews

--allow-trunk 分支只验 required_approving_review_count >= 1reviewDecision == APPROVED,两者都是调用当刻的状态;--auto 把真正的合并推迟给 GitHub。在没开 dismiss_stale_reviews 的仓库上,agent 可以在两项检查通过之后继续 push 未经评审的 commit,auto-merge 随后把它们带进主干——正是这条 rail 存在的理由。Brood 自己 main 开了 dismiss_stale_reviews=true,所以在本仓库是潜伏的;但 git-guard.sh 是作为 pilot 插件分发到任意仓库的。第 234 行的注释专门解释了为什么检查 enforce_admins,却对这个字段只字未提——而它恰恰在 --auto 被放行的那一刻变成承重的。

注意:删掉 --auto 并不能关掉这个窗口 —— gh 自己的 help 写明 merge queue 作为 base 时,不带 flag 也会启用 auto-merge。所以修法在保护检查那一侧:从已经抓到的 $prot JSON 里多解一个 dismiss_stale_reviews 并要求为 true(或在 --allow-trunk 下拒绝 --auto),同时把 234 行注释补上"这个字段为什么是承重的"。

严重度 位置 问题
Low git-guard.sh:197,298 文件头 usage 已改成 [allowlisted gh flags],但两处运行时报错里的 usage 仍写 [gh args]——用户真正看到的那句话还在承诺已经不存在的透传语义
Low git-guard.sh:178-182 gh 官方短形式 -s/-m/-r 被拒,而 -b/-F/-t 短形式放行,不一致。今天没有调用方会断(reference/review-contract.md:54 用的是 --squash),但脚本自己的注释就写着"不可用的守卫会被绕过去"
Low docs/agent/followups.md FU-7 用"pilot 冻结期内不做"做延后理由,但仓库里 grep 不到任何地方声明了这个冻结期,而本 PR 自己就在改 plugins/pilot/

驳回(DeepSeek 的 5 条全部不成立)

  • A1/B1/B2 --match-head-commit —— gh 2.92.0 help 原文:Commit SHA that the pull request head must match to allow merge。它是一个更严的前置条件(head 动过就拒绝合并),不是选择器;值写错 gh 直接拒绝,fail closed;head 分支检查是按名字做的,这个 flag 影响不到。
  • A2 --auto 缺少安全性证明 —— flag 本身不是缺陷,延迟合并窗口经 merge queue 无论如何都存在;已按真实根因重新归档到上面的 Medium。
  • A3 --body-file 可读任意文件 —— 守卫以调用者身份运行,那些文件它本来就能读,没有跨越任何权限边界。
  • Codex F3/F5 —— -b/--bodytext-F/--body-filefile,吃掉下一个 token 是正确行为(pflag 无视前导横杠),后面再来个非法 flag 照样被拒;短粘连形式(-bfoo)被拒是有意的,守卫无法安全拆分,拒绝的方向是对的。

补充(R4 全量复扫)

[Low] git-guard.sh:156 —— 因为 n 在解析循环之前被无条件取走,首位放一个 flag 会被静默当成 PR 选择器,报错还会指错地方。两条都实测过:merge-pr --integration zzz --squash(用户忘了写 PR 号)报的是 refusing extra positional argument 'zzz' … the PR number is the first argument —— 指着 --integration说事;merge-pr --admin --integration zzzbash -x 追踪到 n=--admin,最后死在 cannot read PR #--admin base branch (gh auth / wrong number?) —— 把一个本该被白名单具名拒绝的 flag 说成了认证问题。两条都 fail closed 所以只是 Low,而且加上上面那条 blocking 的一行校验就一起没了。

建议

  • 那条 gh api 走 stdout 的注释修得对,值得留着——把 stderr 折进来确实会破坏 JSON 解析,并让下面每个分支静默退化成"读不到保护规则"。
  • 这属于本仓库一直在追的同一个家族(#39 死代码、#40 随机红灯、FU-2 清不掉东西的清理、FU-7 自己变红的检查):读起来很严、但有一个输入它从来不检查的 rail。建议在 reference/ 里写一条:每个守卫必须校验自己的选择器,而不只是 flag。

PK Review v4 · 4 轮:R1a/R1b DeepSeek-v4-flash(并行,5 条全部被驳回)→ R2 Opus 独立战略评审(亲自跑守卫,产出唯一存活的 Medium)→ R3 Codex 对抗挑战(决定性回合:F4 挑出跨仓库选择器漏洞,前面每一轮都漏了)→ R4 Opus 全量裁决。R2 曾建议降到 2-round,执行器按"安全守卫"保持 4-round,Codex 这一轮证明了这个坚持是对的。机械证据:gh 2.92.0 help 原文、9 组 flag 拒绝实测、gh pr view <URL> --repo <other> 跨仓库覆盖实测、守卫读到外部仓库 PR 的端到端复现。

本地 dist 会随日期漂 ≠ CI 会红。verify.yml 96-122 行在 diff 前把
averageTaskAge pin 到 committed 值(PR#40 做的),所以那条守卫不会
因时钟变红。立 FU-7 时只看了 git status、没读 CI 脚本。

连带 revert 上一个多余的 dist commit。

Claude-Session: https://claude.ai/code/session_01CmAW1q62bBtjT99inZeyLk
评审 R3 挖出的:merge-pr 的第一个位置参数从来没被校验过,而
`gh pr merge` 接受 `[<number>|<url>|<branch>]`。URL 选择器完全无视
`--repo`(gh 2.92.0 实测),于是每道闸门读的都是本仓库(protection、
trunk 判定全用本地解析的 $repo),而 exec 出去的合并落在另一个仓库
—— 拿 Brood 的分支保护当证据,去合别人仓库的 PR。这正是上面拒绝
`--repo`/`-R` 要防的事,拒了 flag 却不看选择器,等于闸门开着。

四条一起修:

1. [Blocking] 选择器必须是纯数字。校验放在解析循环【之前】——放在
   之后的话,`merge-pr --integration x`(忘了写 PR 号)会先死在循环里,
   指着 --integration 的值说『多余的位置参数』,为两个 token 之前的
   错误报错。选择器是最先被消费的,就得最先被判。
   实测:URL 拒 / 分支名拒 / 首位 flag 拒(并指明是漏了 PR 号)/ 纯数字放行。

2. [Medium] --allow-trunk 增加 dismiss_stale_reviews 必须为 true。
   原来的两个证明(保护要求审批、PR 已 APPROVED)都是【调用当刻】的
   快照,而 --auto ——以及 gh 自己 help 写明的 merge queue,不带 flag
   也会——把真正的合并推迟给 GitHub,那一刻本脚本没有任何检查会再跑。
   不开 stale-dismissal 的话,检查通过之后再推的 commit 会顶着旧的
   APPROVED 进主干。从已抓到的 $prot 里多解一个字段,不加 API 调用。
   实测五种形状:true→放行,false/缺字段/无保护/坏 JSON→全部拒。
   并补上 234 行注释:为什么这个字段承重,而 enforce_admins 不承重。

3. [Low] -s/-m/-r 加进白名单。gh 文档里它们是 --squash/--merge/--rebase
   的正式别名,放长拒短显得武断,而武断的守卫会被绕过去。

4. [Low] 两处【运行时】报错的 usage 还写着 `[gh args]`,承诺着已经
   不存在的透传语义,改成 `[allowlisted gh flags]`。

Claude-Session: https://claude.ai/code/session_01CmAW1q62bBtjT99inZeyLk
@jhfnetboy

Copy link
Copy Markdown
Member Author

四条全部成立,已修并推 e9540eb没有一条驳回。

Blocking:选择器

这条我认得很彻底 —— 白名单的全部立意就是「控制什么能到达 gh pr merge」,而它把自己的第一个位置参数完全放过了。你的 URL 覆盖 --repo 那条实测链我在本地复现了同一个形状。

修法按你给的方向,但校验位置往前挪了:放在解析循环之后不够。实测 merge-pr --integration zzz --squash(忘写 PR 号)时,循环会先死在 refusing extra positional argument 'zzz' —— 指着 --integration,为两个 token 之前的错误报错,正是你 R4 补充里那条。选择器是最先被消费的,就得最先被判。挪到 n="${1:-}" 紧后面之后:

输入 现在的报错
merge-pr <NextStop URL> --integration zzz refusing PR selector 'https://…' — pass a bare PR NUMBER. A URL or a branch name overrides the pinned --repo…
merge-pr feat/x --integration zzz 同上
merge-pr --integration zzz --squash refusing '--integration' as the PR selector — the PR NUMBER must come first, before any flag
merge-pr --admin --integration zzz 同上(不再说成 gh 认证问题)
merge-pr 47 --integration zzz -s 正常放行,死在 base 检查

你 R4 那两条 Low 确实一起没了。

Medium:dismiss_stale_reviews

论证成立 —— 两个证明都是调用当刻的快照,而 --auto/merge queue 把真正的合并推迟出去,这脚本在那一刻没有任何检查会再跑。按你说的从已抓到的 $prot 里多解一个字段,不加 API 调用,所以不可能与上面的 approvals 判断打架。

五种形状实测,只有 true 放行:

true      -> [1]     false     -> [0]     缺字段 -> [0]
无保护    -> []      坏 JSON   -> []       (0 和空都走 die)

正路径也走通了:merge-pr 47 --integration main --allow-trunk → 过了 approvals=1 和 dismiss=true,停在 reviewDecision is 'CHANGES_REQUESTED', not APPROVED

234 行注释按你说的补了「为什么这个字段承重、而 enforce_admins 不承重」的对照。

没选「--allow-trunk 下拒绝 --auto」那个方案,理由就是你自己给的:merge queue 不带 flag 也会 auto,拒 flag 治不了根,得在保护检查那侧修。

两条 Low

  • -s/-m/-r 进白名单。gh 文档里是正式别名,放长拒短显得武断,而武断的守卫会被绕过去 —— 这话是这个脚本自己注释里写的。
  • 两处运行时 usage 改成 [allowlisted gh flags]。你说得对:用户真正看到的那句话还在承诺已经不存在的透传语义。

FU-7 那条 Low:我自己先撤回了

你评的是 cc69fbb,8cb1e75 里我已经把 FU-7 整条撤回了 —— 它本身就是误报verify.yml 96–122 行早就在 diff 前把 averageTaskAge pin 到 committed 值(PR#40 做的),CI 根本不会因它变红。我立这条时只看到本地 git status 有 diff 就下了结论,没读 CI 脚本。

所以你那条 Low 说的「grep 不到任何地方声明了这个冻结期」是对的,而且比你想的更严重:延后理由和 bug 本身都不成立。撤回按 append-only 留在账本里没删行。

顺带实证一次:这次推之前 preflight 又让 dist 漂到 99,我没有提交它。这个 PR 的 dist 检查绿不绿,就是那条撤回对不对的判据。

你最后那条建议

每个守卫必须校验自己的选择器,而不只是 flag

这条我不在本 PR 里写进 reference/ —— 不是不认,是这个 PR 已经从「一件事」长回「一件事 + 一条新纪律」了,而它被拆出来的原因恰恰是 #45 装了四件事。记进账本(FU-8),跟 #45 一起收口时落地。

PR#47 review 的建议。那一处实例本 PR 修了,纪律本身没落地。
和 #45 一起收口,不往这个 PR 里加。

Claude-Session: https://claude.ai/code/session_01CmAW1q62bBtjT99inZeyLk

@clestons clestons left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ APPROVE — 上一轮 4 条全部修掉并实测通过 [4-round]

逐条复测(全部在 PR head 上跑真脚本,不是读代码推断):

上一轮的问题 实测结果
[High] 选择器可以是 URL,覆盖被钉住的 --repo merge-pr https://github.com/jhfnetboy/NextStop/pull/26 --integration mainrc=3 BLOCKEDrefs/heads/somebranch26/../../othermain 同样全拒 ✅
[Medium] --allow-trunk 不查 dismiss_stale_reviews 已从同一份已捕获的 $prot 解析并强制要求;两处 parse 不可能互相矛盾 ✅
[Low] -s/-m/-r 被拒 三个都放行并进到 base 检查 ✅
[Low] 运行时 usage 仍写 [gh args] 全文 grep [gh args] → 零命中 ✅
[Low] 首位放 flag 时报错指错地方 merge-pr --integration main --squashrefusing '--integration' as the PR selector,直接点名真正的错误;--admin 同理,且它从未到达白名单

之前拒绝的一律仍拒(--admin / --author-email / -bfoo / 尾随位置参数)。而且把 selector 检查放在解析循环之前这个决定是对的 —— 注释里解释的正是上一轮那条 Low 的成因。

新要求的 dismiss_stale_reviews 不会砸死本仓库自己的流程:gh api repos/AAStarCommunity/Brood/branches/main/protection{"dismiss_stale_reviews":true,"required_approving_review_count":1}

FU-7 的撤回是对的,我独立核实过.github/workflows/verify.yml 96-122 行确实在比对前用 re.subnaverageTaskAge pin 回 git show HEAD: 里的值,并打印 pinned averageTaskAge to committed value。「本地 dist 会漂 ≠ CI 会红」这个自我更正成立,而且那条教训(报「守卫不可靠」之前先读守卫的实现)比 bug 本身有价值。


🔵 建议顺手做掉这一行(零风险,已三 locale 实证)

[Low] git-guard.sh:169 —— *[!0-9]* 里的 0-9 是区间,受 locale collation 影响

这是 Codex 这一轮挑出来的,我复现并定了界:

                        case "٥" (U+0665) in *[!0-9]*)
LC_ALL=C            →   reject
LC_ALL=en_US.UTF-8  →   reject
LC_ALL=fa_IR        →   ACCEPT   ← 当成数字
LC_ALL=fa_IR.UTF-8  →   ACCEPT
LC_ALL=ar_SA.UTF-8  →   ACCEPT

走真脚本:LC_ALL=fa_IR bash git-guard.sh merge-pr "٥" --integration main 不再死在选择器闸门,而是往下走到了 trunk 检查 —— 说明 ٥ 被当成"纯数字 PR 号"接受了。

定界(所以只是 Low):同一 locale 下上一轮那条 High 仍然全部被拦 —— https://…/pull/26refs/heads/x26/../../othermain 都还是 rc=3,因为拉丁字母、/:. 并不在被撑大的区间里。真正漏过的只有 ٥(U+0665) 和 ۵(U+06F5),而它们下一关就死在 gh pr view 上(cannot read PR #٥ base branch → rc=3)。今天没有可达利用,但这个校验器不是 locale-robust,而它是一条安全 rail 的第一道闸门。

修复一行*[!0123456789]* —— 枚举不是区间。已验证在 C / fa_IR.UTF-8 / ar_SA.UTF-8 三种 locale 下都拒绝 ٥


记账(走 #45,本 PR 不要再加东西)

FU-8 白纸黑字写了「不要再往 PR#47 里加东西(它被拆出来就是因为 #45 装了四件事)」—— 同意,下面几条都走 #45

[Medium] git-guard.sh:153 —— 同一个 bug 家族的另一个实例就活在同一文件、修复点上方 170 行

pr-create) 分支以 exec gh pr create "$@" 收尾,零校验:无白名单、不拒 --repo/-R、不查 --base。而它索要的 preflight 戳记绑定的是本仓库 HEAD —— 于是 git-guard.sh pr-create --repo other/repo … 拿本仓库的证明去别的仓库开 PR,与 merge-pr 刚修掉的那处结构完全相同。

FU-8 现在把这条纪律写成「未来风险」("下一个守卫照样可能只查 flag"),但它已经有一个存活实例。建议 FU-8 里把这一处具名成 open 实例,收口时先修它 —— 否则这条 follow-up 读起来像是预防性的,实际是欠账。

严重度 位置 问题
Low git-guard.sh:325 闸门只认 dismiss_stale_reviews:true,但 require_last_push_approval:true 对「批准后再推提交」的防护至少同等强,且就在同一份 $prot 里(Brood 的 payload 里确实有这个字段)。用后者保护的仓库会被无理由拒绝。同一个 parse 加一个 or 即可
Low git-guard.sh:275 保护证明只走 classic protection 端点。该端点对只用 Repository Rulesets 保护的分支返回 404 "Branch not protected",于是 --allow-trunk 在这类仓库上永久不可用 —— 正是脚本注释自己点名的「不可用的守卫会被绕过去」。Brood 自己 rulesets 为空、classic 保护可读,本仓库不受影响;但 plugins/pilot/ 是要装进别的仓库的插件。404 时回落查 repos/$repo/rules/branches/$bpull_request 规则
Low git-guard.sh:14 vs :166 文件头写「2 = usage error, 3 = BLOCKED by a rail」,但纯用法错误全部走 die 返回 3:实测 merge-pr 裸跑 rc=3、merge-pr 26(缺 --integration)rc=3、push origin rc=3,只有未知子命令才 rc=2。调用方按这份文档区分不了「我打错了」和「护栏拒绝了」。本次把 usage 判定从循环后搬进新的 selector case,顺带把这条不一致固化了
Low docs/agent/followups.md:24-25 append-only + 撤回,导致 FU-7 同时以 - [ ](24 行,开)和 - [x](25 行,撤回)存在于同一 id 下,任何「列出未完成 follow-up」的工具仍会把 FU-7 报成 open。原行改成 - [x] FU-7 (superseded, 见下行) 即可

驳回(DeepSeek 4 条全部不成立)

  • A1 -- 未处理 —— -- 落进 -*) 分支 rc=3 fail-closed,不是洞。
  • A2 || true 掩盖 python 错误 —— python 内已 try/except,且 [ "$dismiss" = "1" ] 对空值 fail-closed;pipefail 下去掉 || true 反而会静默 abort、连 die 消息都不打印,更差。
  • B1 dismiss 检查该扩到非 trunk 路径 —— 非 trunk 路径本来就不要求任何 approval,不存在「过期的 approval」;照改会把无保护的 preview 分支直接砸死。
  • B2 -s/-m/-r 绕过了对 --squash/--merge/--rebase 的拒绝 —— 前提就是错的:那三个长形式一直在白名单里,从未被拒;短形式是 gh 2.92.0 --help 里写明的官方 alias。

建议

  • --match-head-commit 已经在白名单里,它正是 gh 提供的「把 head SHA 钉死」的机制。与其硬性要求对方仓库开 dismiss_stale_reviews(很多仓库没开,直接砸死 --allow-trunk),守卫可以在 --auto 场景下自己读当前 head SHA 注入 --match-head-commit —— 自洽、不依赖对方仓库设置,与现有 dismiss 检查可并存互为兜底。
  • FU-7 的撤回建议限定到 averageTaskAgeprojectHealth.staleTasks 来自未 vendored 的外部 CLI,同样可能按天派生,而 verify.yml 并没有 pin 它。

PK Review v4 · 4 轮:R1a/R1b DeepSeek-v4-flash(并行,4 条全部驳回,其中 B2 的前提是事实错误)→ R2 Opus 独立战略评审(自跑 bypass battery,挖出 pr-create 同类洞存活 + 2 条 Low)→ R3 Codex 对抗挑战(CHALLENGE 掉「选择器修复完备」这个断言 —— locale 区间绕过,执行器复现并定界)→ R4 Opus 全量裁决(另补 rulesets 404 与退出码文档不符两条 Low)。机械证据:13 组 selector/flag 实测(含跨仓库 URL、路径穿越、flag 占位、空参);grep '[gh args]' 零命中;verify.yml 96-122 行 pin 逻辑原文核实;gh api .../branches/main/protection 实取;٥ 在 C / en_US / fa_IR / fa_IR.UTF-8 / ar_SA.UTF-8 五种 locale 下的区间 vs 枚举对比。

@jhfnetboy
jhfnetboy merged commit 23d6442 into main Aug 5, 2026
5 checks passed
@jhfnetboy
jhfnetboy deleted the feat/guard-allowlist branch August 5, 2026 18:16
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 5, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants