Skip to content

ci: Robin 改用 zengate 网关访问 big-pickle(删 bootstrap、零 secret) - #67

Merged
by514 merged 8 commits into
SiliconMod:testfrom
Xiaobei09:fix/robin-zengate-gateway
Sep 29, 2026
Merged

by514 merged 8 commits into
SiliconMod:testfrom
Xiaobei09:fix/robin-zengate-gateway

Conversation

@Xiaobei09

@Xiaobei09 Xiaobei09 commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

概览

把自动审查改为 workflow_run 两段式(2026-09-28 定案),删 bootstrap、零 secret、不代发 /review:

段 workflow 事件 干的事
① 采集 robin-collect.yml pull_request(opened/synchronize/reopened/ready_for_review) 只读:checkout + 生成 diff/meta → upload-artifact robin-diff
② 审查 robin-review.yml workflow_run(collect success 后) 默认分支上下文(token 不被 fork 降级,pull-requests: write 真实生效)→ 下载 artifact → 自研驱动 review_from_diff.py 审查 → gh api pulls/{n}/reviews 发 COMMENT

满足四项设计约束:自动触发(workflow_run 跟跑,无 /review 代发)、能发评论(可写 token + review API)、不依赖 GH_PAT(全仓 .github/ 无任何 secrets.* 引用)、只读采集(第①段仅读权限)。

为什么是自研驱动而不是 robin action

R174 实测 antongulin/robin@v2.7.2(及 main 2.8.0)在 workflow_run 事件下落入 No matching trigger found. Skipping. 零产出(action.yml 无 PR/diff/artifact 输入,main.ts 只认 pull_request/issue_comment)。故第②段复用本仓既有的 zengate 网关链路(setup-node/钉版 zengate 1.0.4/起网关/冒烟+模型在售校验)+ .github/code-reviewer.md 规则 + .github/robin.yml 阈值(skip-paths / max-diff-size 80000 / max-comments 12,单一来源),由 review_from_diff.py 完成:skip-paths 过滤 → head-only 截断 → 结构化审查 → 空审查重试 → 行内评论(422 容错)+COMMENT review。

已实测(2026-09-28)

  • 同仓链路(试验床 Xiaobei09/Silicon PR 提交两用存储器的贴图 #37):collect success + artifact 落库;review run 36452000722=success,日志指纹 Posted review 5341739030 on PR #37。
  • 跨仓只读列(本 PR ci: Robin 改用 zengate 网关访问 big-pickle(删 bootstrap、零 secret) #67 自身):fork PR 只读 token 下 Robin Collect run 36452503812=success,robin-diff artifact(22201B)生产落库。第②段跨仓验证随本 PR 合并后自然成立(workflow_run 读默认分支文件)。
  • 两处 heredoc 实证坑已修(并入本 PR):<<'PY' 单引号 heredoc 不展开 $GITHUB_ENV(须 os.environ 读取);GITHUB_ENV 写入只对后续步骤可见(同步骤 echo 自检必 unbound)。均已在试验床 run 实测。

边界修补(R254 十点评审 P1/P3,2026-09-29 落地 · head 9cdc4e4)

补上 09-28 版的三处边界;其余评审点无代码改动要求(#68 票门为独立工单,见其 body):

  • P1 信任链(PR 号 / head SHA / 头仓只从 workflow_run 上下文推导):解析步不再从
    artifact 取 PR 号。候选 PR = workflow_run.pull_requests[0];该数组为空(fork PR 已知
    行为)时按 head={owner}:{branch} 反查 open PR,且必须精确命中 head_sha 与头仓——
    命中 0 个或多个一律 fail-closed。候选 PR 再经 head_sha / 头仓 / state=open 三重核对;
    artifact 只当 diff 内容,其 meta.json 四元指纹(run_id / head_sha /
    head_repo_full_name / number)必须与上下文完全一致,任一不符即不发审查、不猜。
    所有 API 调用(含 pulls/{n}/reviews)只接受上下文推导出的 PR 号。
  • P3 并发 + 去重(一次 synchronize 只发一条 COMMENT):concurrency 按
    头仓 + 头分支 键控 + cancel-in-progress(同 PR 的旧审查 run 被新 run 取代,不串行排队);
    驱动脚本按「github-actions[bot] 已发 COMMENTED 且 commit_id == head_sha」跳过重复,
    兜住取消窗口外的重触发。
  • 输出净化(驱动侧防线):模型输出 / 评论正文剥除控制字符(含 ESC \x1b),防 diff
    或模型输出里的注入串污染评论。

验证(本地 2026-09-29):resolve 步 9 场景(hint 正常 / head 过期 / closed / 反查正常 /
反查 0 个 / 反查多个 / meta run_id / number / head_sha 指纹不符→全 fail-closed)+ 驱动
3 分支(run_id 不符→exit1、head_sha 不符→exit1、已审同 head→跳过)共 12/12 通过;
sanitize 5/5;py_compile + 三个 workflow YAML lint 全绿;远端字节校验 9cdc4e4
(fetch + cmp 三文件逐字节一致)。审查段跨仓 E2E 仍需合并后自然成立(workflow_run 读默认
分支文件),归入下方「待验证」。

票况(head 9cdc4e4,base bd44662b)

请分清两套计票:

计票依据 #67 当前
democracy(action,required context) 不做时间过滤,历史票全算(每人最新一张+作者合成) 自报 4 👍(三张旧票 ARCloud217/rt334/by514 + 作者)⇒ 窗口(16:48:25 起)过后任意 Voting 触发事件即绿
vote_merge.py(本仓严格门,合入时执行) PR 更新后旧票作废(作者合成票保留) 1/3(仅作者合成票),还差 2 张真 APPROVED

democracy 变绿只解除 GitHub 合并按钮,请维护者不要仅凭它点合并——判定够不够票看 vote_merge.py 输出(正票口径:rt334、ARCloud217、by514 需在最新 head 后重新 APPROVED)。

另:保留

  • workflows/robin.yml 保留为手动 /review(issue_comment)路径与同仓自动路径;fork PR 的 pull_request 分支按同仓守卫跳过。
  • 防御细节(网关模型在售校验、冒烟先落盘再判码、use-json-response-mode 语义)已在对应 workflow/驱动中落实。

原方案 pull_request_target → bootstrap 用 GH_PAT 代发 /review → issue_comment
真实审查。它有两个绕不开的问题:

1. 依赖仓库 secret GH_PAT。secret 一旦丢失,bootstrap 走静默 exit 0,
   PR 打开后不再自动审查,且毫无征兆。
2. pull_request_target 把 PR 代码带进高权限上下文,为让审查跑起来不得不
   引入一个持有 repo scope 的个人 PAT。

本改动删除整个 bootstrap,改由 pull_request 事件直连,并在 CI 内用
zengate 起本地网关,由真实的 opencode serve 子进程与 OpenCode Zen 通信:

- 全仓 .github/ 已无任何 secrets.* 引用,github.token 是每次运行自动轮换的
  内建凭据、不是 secret,fork PR 同样拿得到。
- Zen 免费档有五道准入,runner 即便满足前四道仍恒 403;zengate 拉起真实
  opencode 进程与 Zen 通信,绕开的是「来源」那一道,不涉及任何配额或限流。
- 网关自身鉴权用 ALLOW_NO_AUTH 关闭且只监听 127.0.0.1,故全链路无 API key。

两个非显然的关键点:

1. use-json-response-mode: false(必设)
   review-retry.ts 的 shouldRetryStructuredReview 写着 `if (usedJson) return
   false`,即 JSON 模式下空审查一次都不会重试;而 action.yml 里该项默认
   true。模型偶发返回字面量 {} 时会被当成「0 条发现」直接放行——对审查闸门
   是最坏的假通过。关掉后重试保护恢复,实测还更快、发现更多。

2. 冒烟判定必须先落盘再判状态码
   `curl -sf | head -c N` 在 bash -e 下退出码取自管道末端的 head(恒 0),
   网关返 4xx/5xx 时这道闸门依然报通过。改为显式取 %{http_code} 再断言。

另加一处防御:Zen 的免费模型目录会随时间漂移(实测两天内换过一批),就绪
后显式校验目标模型仍在售,不在售则退出 1 并列出当前在售 id,避免下游只看到
「冒烟没答 OK」这种误导性报错。

同时补充 code-reviewer.md:禁止工具调用的声明、项目渲染/连接铁律、15 条
误报红线,以及版本号检查的降级路径措辞收紧。
@Xiaobei09

Copy link
Copy Markdown
Collaborator Author

/review

@Xiaobei09

Copy link
Copy Markdown
Collaborator Author

关于本 PR 上 review 检查显示 cancelled 的说明(不是失败,是预期行为):

本 PR 修改的正是 .github/workflows/robin.yml 自身,而 GitHub 对不同事件取工作流文件的来源不同:

  • pull_request → 用 PR 版本(本 PR 的新网关方案)
  • pull_request_target → 用 base 分支版本(仍是旧的 bootstrap 方案,因为本 PR 尚未合并)

于是两条 workflow 同时在本 PR 上触发,而它们声明的是同一个并发组 robin-67 + cancel-in-progress: true → 后到者取消先到者。

时序证据(排除「评论链触发」这一竞争假设):

事件 时刻 (UTC)
pull_request run(网关)创建 14:43:01
pull_request_target run(base 旧版)创建 14:43:02
网关 run 被取消 14:43:03(创建后仅 2 秒)
旧 bootstrap 发出 /review 评论 14:43:07

取消比 bootstrap 有能力发出评论早 4 秒,所以与评论链无关,只能是并发组互删。

合并后此现象自动消失:base 只剩新 workflow,一条链路一条 run,不再有同组竞争。在 09 仓(base 已是新方案)实测端到端正常——见 PR 描述里的 run 36322853148 / 36323396463 / 36324231772,Gateway ready + smoke HTTP=200 + 真实审查产出 + commitTest 成功。

另:本 PR 上的 democracy 检查失败是既有的票数门槛(Vote has 1 voters, did not have the required min 3 voters),与本改动无关。

Xiaobei09 added a commit to Xiaobei09/Silicon that referenced this pull request Sep 27, 2026
排查发现 action 的投票窗口起点用的字段是错的,且偏差可达数小时。

`git-democracy` 的 main.ts:81 取 `pullRequest.data.head.repo.pushed_at`
当 `voteStartedAt`,voting.ts 用它 + minVotingWindowMinutes 判窗口是否满。
但 `repo.pushed_at` 是**头仓任意分支**的最后 push 时间,与本 PR 无关。

实测 PR SiliconMod#67:
  头提交真实更新时间            2026-09-27T14:42:24Z
  action 用的窗口起点 pushed_at 2026-09-27T16:22:55Z
  偏差                          101 分钟
那次 16:22 的 push 是往同仓的 test 分支推的,与 PR SiliconMod#67 毫无关系。

后果:任何人对头仓任意分支的一次 push,都会把该 PR 的投票窗口向后推。
而全仓 `.github/` 里**没有任何 schedule 触发器**(已 grep 确认三个 workflow
的 on: 只有 pull_request / pull_request_target / pull_request_review /
issue_comment / push / workflow_dispatch),窗口被推之后没有任何事件会
重新评估 → PR 静默卡住,且 required check democracy 会一直红着挡住合并。

本脚本早已改用头提交时间(正确值),但 required check democracy 仍由 action
用 pushed_at 计算,ruleset 卡的还是错的那个值。这是 action 上游的缺陷,
workflow 内无法覆盖,故此处只做两件事:

1. 把两个窗口起点与差值打进日志,让偏差可见(否则只能靠猜)
2. 票数判定全过却被 ruleset 拒合并时,点明真正原因是 required check 而非票数
   —— GitHub 对这种拒绝只回一句 "Pull Request is not mergeable",不提示原因,
   维护者很容易误以为是投票没过。附上具体排查步骤。

同时更正 PR 描述里的一处不准确表述:先前称「票投够后那次评估会因窗口未满
而失败」,实测两次真评估失败(PR#67 14:43、PR#66 06:59)的 voting failure
message **只有「票数不足」一条**、不含窗口那条,说明当时窗口是满足的 ——
当前的阻塞点是票数不足,不是窗口。

验证:47 项断言仍全过。另用 PR SiliconMod#67 的真实时间构造用例,确认偏差被正确算出
(+101 分钟)且合并被拒时给出了排查指引。
@rt334

rt334 commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

逐条看了三个文件的 diff。先说认同的部分:这套改动的注释把「为什么」写清楚了(curl -sf | head 在 bash -e 下被管道末端吞掉退出码、use-json-response-mode: false 与 shouldRetryStructuredReview 的 if (usedJson) return false 互相咬、网关目录轮换要先校验 MODEL 在售、超时预算按最坏路径 50 分钟),这些都是踩过才知道的坑,能写成注释留下来很难得。

有一条我想请你确认——它可能让 Robin 在 fork PR 上整体失效:

1. fork PR 的 GITHUB_TOKEN 是只读的(pull_request 事件)

robin.yml 从 pull_request_target 改成 pull_request,注释里写:

github.token 是 GitHub 自动注入的…不是 secret,所以 fork PR 同样拿得到(pull_request 不注入 secrets.* 这条限制不影响它)。因此本方案对 fork PR 天然可用

"能拿到 token" 这点没问题,但 GitHub 对 fork PR 的 pull_request 事件会把 GITHUB_TOKEN 降级为只读(contents: read,且 workflow 里写 permissions: 也提不上去;这是 pull_request 与 pull_request_target 的核心差别之一)。而 Robin 要发布审查结论需要 pull-requests: write。

如果我的理解没错,那么在本仓最常见的场景(外部贡献者的 fork PR,例如当前 open 的 #50)上,Robin 会拿到 403、发不出任何 review/comment——而现在这条链路之所以能覆盖 fork PR,正是因为有 GH_PAT(PAT 不受"fork PR 降级"约束)。换句话说:去掉 PAT 的同时,可能也去掉了 fork PR 的审查能力,而且失败形态是"没有评论",很容易被当成"这次没发现问题"。

判据可以直接验:在一个 fork 来源的 PR 上触发一次 /robin,看 runner 日志里 Robin 调 POST /repos/.../pulls/N/reviews 的返回码。如果确实是 403,建议二选一:

  • 保留 pull_request(跑 PR 代码、零 secret)但把发布环节交给一个 pull_request_target 的轻量 job(只读 diff、不做 checkout,用 base 仓可写 token 发评论);
  • 或者明确接受"fork PR 只有人工 /robin 且需 PAT"的现状,在 code-reviewer.md/注释里把这条限制写清楚,别让它看起来"天然可用"。

如果我漏了某条让 fork PR 也能写的路径(例如 Robin action 内部走别的凭据),麻烦指出来,这条就可以划掉。

2. npm install -g zengate@^1 是供应链上的一个松口

npm install -g zengate@^1

^1 允许任意 1.x 的新版本,而这个包会在持有 GITHUB_TOKEN 的 runner 上执行、并拉起 opencode 子进程。本仓在别处已经很注意这件事(voting.yml 里把第三方 uses 钉到具体 commit),这里却是"跟随上游大版本",标准不太一致。建议钉到具体版本(zengate@1.x.y,就像注释里关心的 opencode-ai 版本契约一样),升级时手动改一行。


顺带一个与 #67 无关、但会影响本仓节奏的观察:#65 那条 PR 上连发三次 /review 都没有产出审查结果,#67 自己那次 review check 是 cancelled(你已在评论里解释过是工作流文件来源问题)。如果这两件事是同一个根因(网关/触发链路),那本 PR 的优先级应该更高一些——否则 #65 会一直没人审。

@rt334

rt334 commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

补一条与「fork PR 只读 token」互斥的取舍——两条处置路径不能各做一半:

如果为了修 fork PR 而打开仓库的 Send write tokens to workflows from pull requests,要注意 pull_request 事件的 workflow 文件取自 PR 的头分支(这正是它不需要 pull_request_target 的原因)。也就是说,一个 fork PR 可以把 .github/workflows/robin.yml 改成任意内容,然后在带写权限的 run 里执行自己的 run: 步骤——从「只读、发不出评论」变成「外部贡献者能在本仓写权限上下文里跑任意命令」。pull_request_target(本 PR 之前用的)没有这个性质,因为它始终执行 base 分支的 workflow。

所以这两条是互斥的整体决策,不是可以各修一半:

  • 要 fork PR 可写 → 必须把 workflow 的来源钉在 base 分支(回到 pull_request_target + 一个只读 diff 的轻量 job 来发评论,或拆成 workflow_run);
  • 要保持 pull_request(跑 PR 代码、零 secret)→ 就接受 fork PR 上发不出 review,并把这个限制写进注释。

另外 .github/workflows/robin.yml:116-119 的注释现在仍写着「因此本方案对 fork PR 天然可用」——你在 PR 描述里已经把这条更正为"令牌被注入 ≠ 带什么权限",代码注释建议同步改掉,否则下一个读它的人会再踩一次同样的坑(这个失效模式本仓已经踩过一次了)。

回应上游评审(SiliconMod#67)的四点意见,本次只动三处,均在
.github/workflows/robin.yml 内,不引入新依赖、不改审查语义。

1. fork PR 从 pull_request 自动审查路径显式分流
   job 级 if 此前只判 draft,对任何非 draft PR 都放行。但 GitHub 对 fork 来源的
   pull_request 运行会把 GITHUB_TOKEN 降级为只读,而 robin 发 review 评论需要
   pull-requests: write,二者相撞必然 403 → 该 job 在 fork PR 上恒红。
   现补 head.repo.full_name == github.repository 判断:同仓 PR 走自动审查,
   fork PR 改由已存在的 issue_comment /review 路径兜底(该路径在 base 仓上下文
   运行、拿到可写 token,且本方案链路不依赖任何 secret)。

2. 改正「本方案对 fork PR 天然可用」这句与实测相反的注释
   原注释的推理链是「github.token 不是 secret → fork PR 也拿得到 → 天然可用」。
   前半句成立、后半句不成立:拿得到 ≠ 带写权限。已按 R72 的单变量对照实验结论
   改写(同一头提交、只换事件类型,pull_request 报授权错、pull_request_target
   零授权错),并写明不要试图靠开启「Send write tokens to workflows from
   pull requests」来解决——该开关只影响同仓 PR,放宽的却是对 pull_request 运行的
   整体信任面,公共 fork 无论如何都是只读。

3. zengate 由 ^1 改为精确钉 1.0.4
   依据实测:zengate 2026-09-27 01:47 首发,当天 3 小时内连发 5 个版本
   (1.0.0→1.0.4),此后停更。范围写法意味着上游哪天发 1.1.0 就会静默换掉
   本仓网关行为、review 结论随之漂移且事后难归因。
   同时补装后回读校验:装到的版本与钉住的不一致则 ::error:: 退出 1。
   判定走显式 [ ... ] + exit,不依赖管道退出码(末端 cut 恒 0,与本仓
   「curl -sf | head 恒 0」是同一个坑)。提取逻辑已在 4 种 npm 输出形态下实测
   (正常 / 空 / extraneous / invalid)。

旧注释「固定大版本」与实际写法 ^1 矛盾,属误导,本次一并改正。
@Xiaobei09

Copy link
Copy Markdown
Collaborator Author

落实评审意见的提交(本次 head 会移动,请三位重新投一次票)

@rt334 @ARCloud217 @by514 评审意见已全部落实。但要先说清一件事:本次提交会移动 head commit,按本仓门禁「PR 更新后原票不算」的规则,你们现有三张赞成票会当场作废,需要重新投一次。

这不是疏忽,是权衡后的选择:修复里第 1 条是真 bug(此前 fork PR 上这个 job 恒红),拖下去等于让所有跨仓贡献者的自动审查一直是坏的;而等票等不到你回复的收益是零。

先说结论:在当前 head(7e936a3)上,你们三张票是够票的(ARCloud217 14:58:20Z、rt334 15:22:54Z、by514 09-28 06:52:44Z)。我核实过,不是估计——用生产 latest_reviews() 算的,不是手写逻辑。代价就是下面这三点。

改了什么(只动 .github/workflows/robin.yml,+40/−8,1 个文件)

① fork PR 从 pull_request 自动路径显式分流(你的第 1 点,这次是真修,不只是改注释)

job 级 if: 此前只判 draft,对任何非 draft PR 都放行。但 GitHub 对 fork 来源的 pull_request 运行会把 GITHUB_TOKEN 降级为只读,而 robin 发 review 评论需要 pull-requests: write,二者相撞必然 403 —— 这个 job 在 fork PR 上恒红。已补:

github.event.pull_request.head.repo.full_name == github.repository

同仓 PR 走自动审查;fork PR 落到已存在的 issue_comment /review 路径兜底(该路径在 base 仓上下文运行、拿到可写 token,且本方案链路不依赖任何 secret)。

② 改正「本方案对 fork PR 天然可用」那句(你的第 4 点)

原推理链是「github.token 不是 secret → fork PR 也拿得到 → 天然可用」。前半成立、后半不成立:拿得到 ≠ 带写权限。按单变量对照实验(同一次提交、只换事件类型,pull_request 报授权错、pull_request_target 零授权错)改写。

③ 为什么不靠开「Send write tokens to workflows from pull requests」解决(你的第 3 点)

该开关只影响同仓 PR;为它放宽的却是对 pull_request 运行的整体信任面;且公共 fork 无论开关如何都是只读。所以它解决不了这个问题。本方案的取舍:自动审查只覆盖同仓 PR,fork PR 走 /review。

④ zengate@^1 → 精确钉 1.0.4(你的第 2 点)

依据实测:该包 2026-09-27 01:47 首发,当天 3 小时连发 5 个版本(1.0.0→1.0.4),此后停更。范围写法意味着上游哪天发 1.1.0 就会静默换掉本仓网关行为,review 结论随之漂移且事后难归因。

同时补装后回读校验,装到的版本与钉住的不一致则 ::error:: 退出 1。判定走显式 [ ... ] + exit,不依赖管道退出码(末端 cut 恒 0,与本仓「curl -sf | head 恒 0」是同一个坑)。提取逻辑已在 4 种 npm ls 输出形态下实测(正常/空/extraneous/invalid)。

顺带:旧注释「固定大版本」与实际写法 ^1 矛盾,属误导,一并改正。

麻烦三位了

请在推完之后重新 review 一次(不必重新写内容,APPROVED 即可)。门槛是 3 张有效赞成票。

如果谁希望先合并旧版、等修复另开 PR,说一声我就 revert 这个提交。

@Xiaobei09

Copy link
Copy Markdown
Collaborator Author

补充两点(不影响投票,只影响怎么看 robin 的输出)

① 本 PR 改的 robin 配置,在合并前不会生效

查 antongulin/robin@v2.7.2 的 action.yml 原文:

review-instructions-file: "…Read from the PR base branch for safety."
config-file:              "…repository config file on the PR base branch (.github/robin.yml)."

两个文件都从 PR 的 base 分支读,不是 head。所以:

  • 本 PR 对 .github/robin.yml(+16/−1)和 .github/code-reviewer.md(+32/−1)的改动
  • 在合并之前,robin 这次审查读到的仍是 base 上的旧版配置
  • 包括 code-reviewer.md 里新加的「你没有可读文件系统、禁止调用工具」那条防幻觉指引

这不影响各位投票与否(票投的是改动本身,不是 robin 这次审得准不准),但看 robin 的评论时需要知道:它这次并不知道我们给它补了哪些规则。

顺带解答一个可能的疑问:工作流里没有 actions/checkout 却能传 config-file —— 因为 action 走 API 读 base,不读工作区。

② 头提交时间的权威口径(澄清一个我之前没说准的地方)

「PR 更新后原票作废」这条规则,代码里取的是

update_time = parse_ts(head_commit["commit"]["committer"]["date"])

即 head commit 的 committer date,不是 pull.updated_at。

用生产代码跑出来的当前状态:

PR head update_time 有效票
#67 1a3d9f0 2026-09-28 07:12:50Z 0(三张全在此之前)
#68 5885cc3 2026-09-28 05:34:35Z 2(by514 有效,ARCloud217 已过期)

⇒ 由此可知:在 PR 里发评论不会让票作废,只有推新 head 才会。上一条评论我不会导致各位重投,抱歉之前没讲清。

@Xiaobei09

Copy link
Copy Markdown
Collaborator Author

/review

@Xiaobei09

Copy link
Copy Markdown
Collaborator Author

一条影响评审判断的事实:当前 head 1a3d9f0 从未被 Robin 审过,且我无法触发

我刚发了一条 /review 想补上,run 的结果是:

config-check  success
review        success   ← 但只跑了 5 秒
日志: ##[warning]Ignoring /review from Xiaobei09; minimum permission is triage.

「成功」是静默空跑,什么都没产出。 原因是本仓 workflow 和 robin action 的两道门依据不同:

门 依据 我的情况(author_association=COLLABORATOR)
.github/workflows/robin.yml 的 if: author_association ∈ {OWNER, MEMBER, COLLABORATOR} 放行
action 的 min-command-permission: triage repos.getCollaboratorPermissionLevel().permission(真实仓库角色) 拒绝

⇒ 我的真实仓库角色低于 triage,所以 /review 恒被忽略;workflow 里 COLLABORATOR 那条放行实际是死代码。

这对各位评审的实际含义:这个 head 的正确性目前只靠 265 条断言的回归套件 + 真实快照实跑(自举前后各跑一遍,失败原因逐字对得上)支撑,没有 AI 第二意见。如果需要,可以由有 push 权限的维护者打 /review 补上。

(这两处都不是本 PR 引入的 —— min-command-permission: triage 在 base 的 robin.yml:89 早已存在,所以我不动它。)

@Xiaobei09

Copy link
Copy Markdown
Collaborator Author

补充:democracy 那个红叉是时间窗假象,重投票就会同时跑绿

#67 现在 mergeStateStatus=BLOCKED,唯一原因是 required context democracy 红了。但它不是票的问题:

##[error]vote failed: - Vote requires a minimum voting window of 10 minutes
before passing a vote. Earliest time to end window is Mon Sep 28 2026 07:39:54 GMT+0000

该 run 在 07:30:10 执行,距本次 push(07:29:54)只过了 16 秒 —— action 把「窗口还没满 10 分钟」当成了硬错误。而这是 1a3d9f0 上唯一一次 Voting 运行,之后没有新事件在 head 上重跑过。

为什么我发的几条评论没让它重跑:issue_comment 触发的 run 挂在 base 提交上(sha=bd44662),不出现在本 PR 的 check rollup、也不满足 head 上的 required context。而 pull_request_review 触发的 run 挂在 head 上。

⇒ 所以:任何一位评审重新投一次票,会同时发生两件事 —— 补上票数、并把 democracy 跑绿(此刻距 push 已 3 小时以上,10 分钟窗口早已满足)。维护者不需要额外做任何操作。

当前有效票:1/3(仅作者合成票;此前三张真票都早于 1a3d9f0 的提交时间,已作废)。还差 2 张。

Xiaobei08 and others added 6 commits September 28, 2026 19:08
R130  去掉 `tail -1`:npm 的 `ls` 输出在树之后会补一个空行,`tail -1` 取到的是
      那个空行而非 `└── zengate@ver`,sed 永不匹配 ⇒ `$got` 恒空 ⇒ 这道版本回读
      校验**每次必失败**。改用 `sed -n 's/...p' | head -1`。
      复现:`cat -A` 证实树后确有空行;旧写法 got=[] 判失败,新写法 got=1.0.4 判通过。

R135  读嵌套的 opencode-ai:npm 安装全局包时**不提升**依赖,原写法
      `npm ls -g opencode-ai` 查的是全局顶层,读到的必然是空(实测输出 `(empty)`),
      而 binary.js 实际加载的是 $P/zengate/node_modules/opencode-ai。改为直接读那份
      package.json 的 version(实测 0.1.2,与嵌套包一致)。

R132  订正注释里一处夸大的实测表述:原注释称已验证「同仓 PR 走自动路径正常」,
      实际那次验证只确认了 fork 判定**没有误伤**同仓 PR(结论是 failure 而非 skipped),
      并**未**验证其最终 review 成功。改为按实际验证范围陈述,并补上未验证项。
上一版注释标着「2026-09-27 实测修正」,实测复核后四个数全错,且错误互相加固:

1. 归属错:#16=501252 等五个数逐字吻合的是 fork Xiaobei09/Silicon,
   而本仓 SiliconMod/Silicon 的 #16/#17 REST 直接 404。这是本项目
   第 4 次栽在 fork/本仓混用上。
2. 推论错:「调到 200000 可覆盖 #17/#10/#15」在它自己列的数上就不成立
   (292127/307805/302371 全 >200000)。
3. 占比错:「仅剩 16%–32%」真值 16.0%–27.4%,上界虚高 4.6 个百分点。
4. 倍数错:「字符数是行数的 2–3 倍」实测 45–58 倍,且全仓最大 diff
   只有 9083 行,不存在 170k 行的 PR。

改动:
- 重写注释:逐条标仓名、给出本仓真实分布(本仓最大 #10=130167,
  80000 仍剩 61.5%)、写死度量口径为「API 响应原始字节数」
  (经 shell $( ) 量会剥掉尾部换行,每个 PR 少 1–3 字符)。
- 删掉站不住的「行数 vs 字符数 2–3 倍」推断,不留作依据。
- max-diff-size 的**值不动**(仍是 80000):改它是行为变更、不在本次
  评审点范围内,注释里给足数据与建议,由维护者定。
- code-reviewer.md:第 9 与第 12 条标题重名(「arc API 特殊性不报误用」),
  人工引用无法区分,改第 12 条为「arc 工具类/样式 API 特殊性不报误用」。

新增闸门(根因治理,不只修这一次):
- checks/check-robin-yml.py:把注释里每个 #NN=NNNN 从 GitHub API
  逐个重新推导;校验截断占比算术、「N 可覆盖 X」类推论、跨仓存在性;
  注释块必须逐条标仓名。取不到输入判失败;404 与瞬时故障分开报。
- checks/mutate-robin-yml.py:6 个反向变异(含复活旧错账、404 路径、
  引文豁免双向)全部转红。

验证:check-robin-yml 14/14、mutate-robin-yml 6/6、
AGENTS.md 三检查、voting.yml 结构台 16/16 + 变异 5/5、
vote_merge 行为台 265/265。
- fork PR 403 声明:锚定 2026-09-27 双事件对照(run 36357503139 vs
  36337473071,头提交 a56c600,唯一变量是事件类型),并注明 robin
  自身 pull_request 仅 3 次运行、全为 cancelled/skipped、无 403 日志,
  降级行为按 GitHub 文档延伸。
- 「正常路径 2-4 分钟」:注明实测范围是 09 仓测试 PR #28 的 CI
  (旧链路设 OPENCODE_PATH),run 36319218561≈114s、run 36319493968≈233s,
  本工作流在生产尚无完整 run,仅供预算留量参考。纯注释改动,零行为变化。
testbed R182/R183 实证:<<'PY' 单引号 heredoc 不展开 $GITHUB_ENV(写字面文件名
静默建空文件);且 GITHUB_ENV 写入只对后续步骤可见,同步骤 echo 在 set -u 下
unbound variable 杀步。两处一并修复(=试验床已验证内容 a3b9014)。
R254 边界评审 P1/P3 落地(2026-09-29):
- P1: 解析步只从 workflow_run 上下文推导 PR 号/head SHA/头仓;artifact 仅作 diff+指纹;
      meta.run_id/head_sha/head_repo/number 四元指纹与上下文不符即 fail-closed;
      pull_requests 为空时按 head=owner:branch 反查 open PR 且须精确命中 head_sha+头仓。
      resolve 步把 REVIEW_RUN_ID 写入 GITHUB_ENV,驱动侧 --run-id 三重指纹兜底(本提交修复接线缺口)。
- P3: concurrency group=头仓:头分支 + cancel-in-progress(per-PR 并发);
      驱动按 github-actions[bot] COMMENTED + commit_id==head_sha 去重,一次 synchronize 只发一条;
      sanitize_text 剥控制字符(含 ESC)防注入串污染评论。
- 本地测试 12/12(resolve 9 场景 + 驱动指纹/去重 3 分支)+ sanitize 5/5 + py_compile/YAML lint 全绿。
@by514

by514 commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

/robin

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

🏹 Robin

⚠️ I couldn't finish the review this time.

Reason:

Free model routes drop sometimes — comment /robin to try again. (No secrets are included in this message.)

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Voting is closed Voting
Vote on this by posting a PR with approval or needs changes.

Vote Summary:
4 👍
0 👎

Acceptance Criteria:

  • 100% of weighted votes needs to be to approve
  • 3 minimum # of unique voters required
  • at least 10 minutes of voting

@by514
by514 merged commit 12b96ef into SiliconMod:test Sep 29, 2026
6 of 9 checks passed
ARCloud217 pushed a commit to ARCloud217/Silicon-ARCforked that referenced this pull request Oct 2, 2026
…d#67)

* ci: Robin 改用 zengate 网关访问 big-pickle(删 bootstrap、零 secret)

原方案 pull_request_target → bootstrap 用 GH_PAT 代发 /review → issue_comment
真实审查。它有两个绕不开的问题:

1. 依赖仓库 secret GH_PAT。secret 一旦丢失,bootstrap 走静默 exit 0,
   PR 打开后不再自动审查,且毫无征兆。
2. pull_request_target 把 PR 代码带进高权限上下文,为让审查跑起来不得不
   引入一个持有 repo scope 的个人 PAT。

本改动删除整个 bootstrap,改由 pull_request 事件直连,并在 CI 内用
zengate 起本地网关,由真实的 opencode serve 子进程与 OpenCode Zen 通信:

- 全仓 .github/ 已无任何 secrets.* 引用,github.token 是每次运行自动轮换的
  内建凭据、不是 secret,fork PR 同样拿得到。
- Zen 免费档有五道准入,runner 即便满足前四道仍恒 403;zengate 拉起真实
  opencode 进程与 Zen 通信,绕开的是「来源」那一道,不涉及任何配额或限流。
- 网关自身鉴权用 ALLOW_NO_AUTH 关闭且只监听 127.0.0.1,故全链路无 API key。

两个非显然的关键点:

1. use-json-response-mode: false(必设)
   review-retry.ts 的 shouldRetryStructuredReview 写着 `if (usedJson) return
   false`,即 JSON 模式下空审查一次都不会重试;而 action.yml 里该项默认
   true。模型偶发返回字面量 {} 时会被当成「0 条发现」直接放行——对审查闸门
   是最坏的假通过。关掉后重试保护恢复,实测还更快、发现更多。

2. 冒烟判定必须先落盘再判状态码
   `curl -sf | head -c N` 在 bash -e 下退出码取自管道末端的 head(恒 0),
   网关返 4xx/5xx 时这道闸门依然报通过。改为显式取 %{http_code} 再断言。

另加一处防御:Zen 的免费模型目录会随时间漂移(实测两天内换过一批),就绪
后显式校验目标模型仍在售,不在售则退出 1 并列出当前在售 id,避免下游只看到
「冒烟没答 OK」这种误导性报错。

同时补充 code-reviewer.md:禁止工具调用的声明、项目渲染/连接铁律、15 条
误报红线,以及版本号检查的降级路径措辞收紧。

* ci: 修正 robin 工作流对 fork PR 的凭据假设,并精确钉住 zengate 版本

回应上游评审(SiliconMod#67)的四点意见,本次只动三处,均在
.github/workflows/robin.yml 内,不引入新依赖、不改审查语义。

1. fork PR 从 pull_request 自动审查路径显式分流
   job 级 if 此前只判 draft,对任何非 draft PR 都放行。但 GitHub 对 fork 来源的
   pull_request 运行会把 GITHUB_TOKEN 降级为只读,而 robin 发 review 评论需要
   pull-requests: write,二者相撞必然 403 → 该 job 在 fork PR 上恒红。
   现补 head.repo.full_name == github.repository 判断:同仓 PR 走自动审查,
   fork PR 改由已存在的 issue_comment /review 路径兜底(该路径在 base 仓上下文
   运行、拿到可写 token,且本方案链路不依赖任何 secret)。

2. 改正「本方案对 fork PR 天然可用」这句与实测相反的注释
   原注释的推理链是「github.token 不是 secret → fork PR 也拿得到 → 天然可用」。
   前半句成立、后半句不成立:拿得到 ≠ 带写权限。已按 R72 的单变量对照实验结论
   改写(同一头提交、只换事件类型,pull_request 报授权错、pull_request_target
   零授权错),并写明不要试图靠开启「Send write tokens to workflows from
   pull requests」来解决——该开关只影响同仓 PR,放宽的却是对 pull_request 运行的
   整体信任面,公共 fork 无论如何都是只读。

3. zengate 由 ^1 改为精确钉 1.0.4
   依据实测:zengate 2026-09-27 01:47 首发,当天 3 小时内连发 5 个版本
   (1.0.0→1.0.4),此后停更。范围写法意味着上游哪天发 1.1.0 就会静默换掉
   本仓网关行为、review 结论随之漂移且事后难归因。
   同时补装后回读校验:装到的版本与钉住的不一致则 ::error:: 退出 1。
   判定走显式 [ ... ] + exit,不依赖管道退出码(末端 cut 恒 0,与本仓
   「curl -sf | head 恒 0」是同一个坑)。提取逻辑已在 4 种 npm 输出形态下实测
   (正常 / 空 / extraneous / invalid)。

旧注释「固定大版本」与实际写法 ^1 矛盾,属误导,本次一并改正。

* fix(robin): 修正 zengate 回读校验的三个实测缺陷

R130  去掉 `tail -1`:npm 的 `ls` 输出在树之后会补一个空行,`tail -1` 取到的是
      那个空行而非 `└── zengate@ver`,sed 永不匹配 ⇒ `$got` 恒空 ⇒ 这道版本回读
      校验**每次必失败**。改用 `sed -n 's/...p' | head -1`。
      复现:`cat -A` 证实树后确有空行;旧写法 got=[] 判失败,新写法 got=1.0.4 判通过。

R135  读嵌套的 opencode-ai:npm 安装全局包时**不提升**依赖,原写法
      `npm ls -g opencode-ai` 查的是全局顶层,读到的必然是空(实测输出 `(empty)`),
      而 binary.js 实际加载的是 $P/zengate/node_modules/opencode-ai。改为直接读那份
      package.json 的 version(实测 0.1.2,与嵌套包一致)。

R132  订正注释里一处夸大的实测表述:原注释称已验证「同仓 PR 走自动路径正常」,
      实际那次验证只确认了 fork 判定**没有误伤**同仓 PR(结论是 failure 而非 skipped),
      并**未**验证其最终 review 成功。改为按实际验证范围陈述,并补上未验证项。

* fix(robin): 逐条修正 max-diff-size 注释的四处错账,并加可复算闸门

上一版注释标着「2026-09-27 实测修正」,实测复核后四个数全错,且错误互相加固:

1. 归属错:SiliconMod#16=501252 等五个数逐字吻合的是 fork Xiaobei09/Silicon,
   而本仓 SiliconMod/Silicon 的 SiliconMod#16/SiliconMod#17 REST 直接 404。这是本项目
   第 4 次栽在 fork/本仓混用上。
2. 推论错:「调到 200000 可覆盖 SiliconMod#17/SiliconMod#10/SiliconMod#15」在它自己列的数上就不成立
   (292127/307805/302371 全 >200000)。
3. 占比错:「仅剩 16%–32%」真值 16.0%–27.4%,上界虚高 4.6 个百分点。
4. 倍数错:「字符数是行数的 2–3 倍」实测 45–58 倍,且全仓最大 diff
   只有 9083 行,不存在 170k 行的 PR。

改动:
- 重写注释:逐条标仓名、给出本仓真实分布(本仓最大 SiliconMod#10=130167,
  80000 仍剩 61.5%)、写死度量口径为「API 响应原始字节数」
  (经 shell $( ) 量会剥掉尾部换行,每个 PR 少 1–3 字符)。
- 删掉站不住的「行数 vs 字符数 2–3 倍」推断,不留作依据。
- max-diff-size 的**值不动**(仍是 80000):改它是行为变更、不在本次
  评审点范围内,注释里给足数据与建议,由维护者定。
- code-reviewer.md:第 9 与第 12 条标题重名(「arc API 特殊性不报误用」),
  人工引用无法区分,改第 12 条为「arc 工具类/样式 API 特殊性不报误用」。

新增闸门(根因治理,不只修这一次):
- checks/check-robin-yml.py:把注释里每个 #NN=NNNN 从 GitHub API
  逐个重新推导;校验截断占比算术、「N 可覆盖 X」类推论、跨仓存在性;
  注释块必须逐条标仓名。取不到输入判失败;404 与瞬时故障分开报。
- checks/mutate-robin-yml.py:6 个反向变异(含复活旧错账、404 路径、
  引文豁免双向)全部转红。

验证:check-robin-yml 14/14、mutate-robin-yml 6/6、
AGENTS.md 三检查、voting.yml 结构台 16/16 + 变异 5/5、
vote_merge 行为台 265/265。

* fix(robin): 两处「实测」注释补齐可复现范围与 run id 锚点

- fork PR 403 声明:锚定 2026-09-27 双事件对照(run 36357503139 vs
  36337473071,头提交 a56c600,唯一变量是事件类型),并注明 robin
  自身 pull_request 仅 3 次运行、全为 cancelled/skipped、无 403 日志,
  降级行为按 GitHub 文档延伸。
- 「正常路径 2-4 分钟」:注明实测范围是 09 仓测试 PR SiliconMod#28 的 CI
  (旧链路设 OPENCODE_PATH),run 36319218561≈114s、run 36319493968≈233s,
  本工作流在生产尚无完整 run,仅供预算留量参考。纯注释改动,零行为变化。

* ci(robin): 跨仓审查改 workflow_run 两段式(collect+review 驱动),零 secret、不代发 /review

* fix(robin): review 二段 heredoc 下 $GITHUB_ENV 必须经 os.environ 且本步不 echo 自检

testbed R182/R183 实证:<<'PY' 单引号 heredoc 不展开 $GITHUB_ENV(写字面文件名
静默建空文件);且 GITHUB_ENV 写入只对后续步骤可见,同步骤 echo 在 set -u 下
unbound variable 杀步。两处一并修复(=试验床已验证内容 a3b9014)。

* fix(robin): P1 指纹链完整接线(run_id 从 resolve 步传给驱动)+ P3 并发/去重/输出净化

R254 边界评审 P1/P3 落地(2026-09-29):
- P1: 解析步只从 workflow_run 上下文推导 PR 号/head SHA/头仓;artifact 仅作 diff+指纹;
      meta.run_id/head_sha/head_repo/number 四元指纹与上下文不符即 fail-closed;
      pull_requests 为空时按 head=owner:branch 反查 open PR 且须精确命中 head_sha+头仓。
      resolve 步把 REVIEW_RUN_ID 写入 GITHUB_ENV,驱动侧 --run-id 三重指纹兜底(本提交修复接线缺口)。
- P3: concurrency group=头仓:头分支 + cancel-in-progress(per-PR 并发);
      驱动按 github-actions[bot] COMMENTED + commit_id==head_sha 去重,一次 synchronize 只发一条;
      sanitize_text 剥控制字符(含 ESC)防注入串污染评论。
- 本地测试 12/12(resolve 9 场景 + 驱动指纹/去重 3 分支)+ sanitize 5/5 + py_compile/YAML lint 全绿。

---------

Co-authored-by: Xiaobei09 <Xiaobei09@users.noreply.github.com>
Co-authored-by: xiaobei08 <89635391+xiaobei08@users.noreply.github.com>
Co-authored-by: opencode <opencode@local>
Xiaobei09 added a commit to Xiaobei09/robin that referenced this pull request Oct 2, 2026
生产实证(SiliconMod/Silicon#67,run 36585231278):一条 `Reason: ` 后面**什么都没有**
的失败评论。维护者看到它只能推断「Robin 挂了」,而挂掉的原因恰恰是最该被解释的那件事。

三件事在 errorMessage 这一个收敛点修掉:

1. **空 message 必须兜底。** `new Error("")` 的 message 是空串,instanceof Error 分支
   会把它原样返回,于是评论里那行等于不存在。改成给明确的无信息量占位
   (`HttpError (no message)` 之类),宁可承认没信息,也不要留空。
2. **优先取 response.data.message。** octokit RequestError 的 message 只是壳
   (`Request failed due to error response: 403`),真正的原因在
   `response.data.message`(例如 `Resource not accessible by integration`,正是 fork PR
   token 被降级成只读时的那个)。不取的话日志只有状态码,唯一有用的信息被丢掉。
3. **白名单取字段,绝不整体序列化 response。** response.data 在 OAuth 错误分支里会带
   client_secret / access_token,而 errorMessage 的输出会进**公开的 PR 评论** ——
   那条评论还写着 "No secrets are included in this message"。

顺带:输出单行化(换行会折断 `Reason: ` 那一行的可读性)、400 字符截断(评论不是日志)。

变异:M12 去掉空兜底 → 抓住 2 条;M13 不取 response.data.message → 抓住 1 条;
M14 白名单换成 Object.keys 全遍历 → **首轮假通过**。

M14 那次是本轮最值得记的:我把 client_secret 排在 message **后面**,而 Object.keys
按插入序遍历,所以变异会先返回 message 而看起来安全。实测「secret 在前」的组合
真的返回 `cs_SUPERSECRET`。⇒ 把测试数据改成最坏插入序后,M14 被本该抓它的那条
断言抓住。教训:**断言的样本顺序也是判据的一部分**,对抗「按遍历顺序取第一个字段」
这类变异时,必须把危险项排在前面。

回归:tsc 绿,17 套件 265 断言全绿(新增 8 条)。本提交只含源码,dist 下轮。
Xiaobei09 added a commit to Xiaobei09/robin that referenced this pull request Oct 2, 2026
镜像提交。逐字节来自 upstream 1818227(dist blob d11cea8…,action.yml 未变)。

生产实证 SiliconMod/Silicon#67(run 36585231278)留下一条 `Reason: ` 后面什么都没有
的失败评论。现在 errorMessage 三件事收口:空 message 给明确占位而不留空;优先取
response.data.message(octokit 的壳只有状态码,真正的原因在那里面);白名单取字段
而非整体序列化 response —— 后者会把 OAuth 分支里的凭据写进公开 PR 评论。

Silicon 钉的是 @main 浮动 ref,推送即生效。
Xiaobei09 added a commit to Xiaobei09/robin that referenced this pull request Oct 2, 2026
R939 在 `errorMessage()` 上加了 8 条断言、跑了 3 个变异,全绿。但 `main.ts` 从头到尾
**没有 import 过 `errorMessage`** —— 它的 catch 一直在用内联三元
`error instanceof Error ? error.message : String(error)`。生产上那条空 `Reason:`
评论(SiliconMod/Silicon#67,run 36585231278,comment 5892637288)正是从这行产生的。

也就是说 R939 的修复**对线上零效果**,而测试套件对此毫无反应。

这是「helper 有单测 ≠ 调用点用上了」第 6 次现身(R926 / R934 / R937 / R939)。

本轮三处改动:

1. `main.ts` 的 catch 改走 `errorMessage(error)`。
   只改「给人看的文本」:换 CI 的判定收的是**原始 `error`**(`planRelaunch` 内部自己
   分类、不收 `errorText`),所以永久性错误的 fail-closed 完全不受影响。

2. `buildFailedStatusBody` 从 `main.ts` 搬进 `status-comment.ts` 并导出。
   原因:它在 `main.ts` 里是模块私有函数,而 `main.ts` 一被 import 就整个 `run()`
   起来 —— 这类函数永远拿不到测试,于是只能去测旁边那个「长得像」的 helper。
   搬出来之后,生产组合本身可测。

3. 渲染点再做一次空值兜底。上游已经兜了,这里是刻意的冗余:空 Reason 恰恰出现在
   最需要解释的那次失败上,而多写一个常量的成本是零。

## 变异

| 变异 | 结果 |
|---|---|
| M15 catch 退回内联三元(= R939 修复未生效时的线上状态) | ✅ 2 条 |
| M16 去掉渲染点兜底 | ✅ 2 条 |
| M17 把人类可读文本重包成 Error 喂进换 CI 判定 | ⚠️ 首轮**存活** |
| M17 收紧断言后重做 | ✅ 1 条 |

M17 首轮存活的原因值得记:断言写成 `expect(callSite).toContain("error,")`,
而 M17 把 `error,` 换成 `error: new Error(message),` 后,切片里**别处**仍有
`error,` 可命中。收紧成匹配 `error:` 的属性值后才被抓住。

更重要的一个教训来自收紧过程中的**基线报红**:我把断言写成 `error:\s*error\s*,`,
而 `main.ts` 用的是 ES shorthand `error,`(根本没有 `error:` 键)—— 结果基线自己
就红,M18 的三条报红里有一条是这个假阳性造成的。**变异验证的前置条件是基线必须先绿**,
否则「抓住变异」这句话本身没有意义。这与 R937 的「M11 编译失败 ≠ 变异存活」同源。

## 回归

tsc 绿,17 套件 275 断言全绿(新增 10 条,其中 4 条是接线断言)。
接线断言比对前先剥注释 —— 解释这次 bug 的注释本身就写着那个内联三元,
朴素的「源码里不许出现」会因为注释而恒真失败。

本提交只含源码,dist 下轮。
Xiaobei09 pushed a commit to Xiaobei09/robin that referenced this pull request Oct 2, 2026
生产取证时撞见的真问题(不是体积,是 marker):SiliconMod/Silicon#67 的失败评论
id=5892637288(2026-09-29)实测 has_marker=false。

后果不是「少个看不见的东西」,而是**整条收养链断掉**:findLatestStatusComment 只认
带 marker 的评论,一旦某条状态评论没戴 marker,下一轮就看不见它、于是新建一条而
不是更新它 —— 这正是 status-comment.ts 开头要解决的「评论堆积」,而且退化时完全
无声:评论照发,只是越攒越多。

已核实成因:ac3113c(2026-10-02)才引入 decorateStatusCommentBody 与收养机制,
那条评论产于 09-29,早于该提交 ⇒ 属预期历史行为、不是现存缺陷。

但真正的问题在这里:守卫修好之后,没为它留任何断言。R941 的 4 条接线断言覆盖了
errorMessage 与 buildFailedStatusBody,唯独没覆盖 marker 装饰。将来只要新增一条
状态评论写入路径而忘了装饰,收养链静默断掉、评论开始堆积,而测试全绿 —— 这正是
R941 那一族缺陷的成因模式本身,被复现在了守卫上。

判据是「body: 的**值**就是装饰调用」,不是「函数体里出现过这个标识符」:
- 断言按 async function 起止做函数切片;搜全文会被 main.ts 里那行 import 恒真命中。
- 只搜标识符会被 M29 存活:把装饰改写成 `decorateStatusCommentBody(body);` 之后
  仍然 `body,` 原样发出 —— 标识符在,行为已坏。这与 R937 的 M17、R943 的脆弱断言
  同族:搜标识符 ≠ 钉住接线。

变异:M27 update 路径不装饰抓 1;M28 create 路径不装饰抓 1;M29 死调用(收紧前存活,
收紧后抓 1);M30 create 路径死调用抓 1;M31 装饰挪到函数末尾结果被丢弃抓 1。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants