Skip to content

feat(autofix): run one bounded self-review before an address-review commit (A/B) - #11516

Merged
wenshao merged 2 commits into
mainfrom
autofix-inround-self-review-ab
Sep 10, 2026
Merged

wenshao merged 2 commits into
mainfrom
autofix-inround-self-review-ab

Conversation

@wenshao

@wenshao wenshao commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds an opt-in, A/B-measured in-round self-review to the autofix address-review lane. When a round is armed, it runs ONE bounded adversarial review of its own delta (the same review run the local /autofix mode uses, at high effort) after the trusted checks pass and before it commits, applies what that pass finds, and pushes. The round then records what happened in a machine-read file; the verification gate validates that record, binds it to the tree id of the commit being pushed, and the round report carries an <!-- autofix-self-review … --> marker so the experiment can be read straight off the PR thread. Nothing rejects: the gate only measures.

The arm is the repository variable QWEN_AUTOFIX_SELF_REVIEWoff (default), ab (odd PR numbers armed, even ones the control arm) or on — resolved once per round in prepare and never on a GitHub-hosted runner. An armed round gets a 180-minute agent budget under a per-arm step cap (190 armed, 130 unarmed), skips the same-run repair chain, and skips the pass itself when the delta is under 150 changed lines or fewer than 75 minutes remain. The skill reads the arm from three Invocation lines (arm, CLI entry, round deadline) and nothing else.

Why it's needed

Measured on all 40 autofix/takeover PRs on 2026-09-10 (79 acted rounds, 1369 inline findings, each blamed at its review head): after a round pushes, 73% of the next review's new Criticals and 93% of its Suggestions sit on that round's own delta; 54% of those reviews had every new finding on the delta, and 62% would have posted nothing under the critical floor. Each pushed round costs a full cycle of about 870 minutes median (agent 118, review turnaround 289, idle to the next round 348). One fresh pass over the delta before the push therefore has the right scope, and its ceiling is that 54–62% of rounds.

The same measurement fixes the shape. Critical density is 0.53 per 100 changed lines on bot deltas against 0.52 on human deltas — about 2 new Criticals per review whoever pushed, with no decay across rounds. The reviewer's yield on a fresh delta is the invariant, not the fixer's code, so "review until clean" cannot converge inside a round; it would only relocate the churn into a step with a hard budget whose overrun feeds the failure breakers. Hence one pass, bounded, measured.

Reviewer Test Plan

How to verify

  • Read design entry af-156 in the workflow design doc for the mechanism, the arithmetic behind the step caps, and the deliberate limits.
  • The scripts test file covers: arm resolution (parity, on/off, a hosted runner never arms), the three Invocation lines the runner prints and the values it refuses, the skill's one-pass rule and its CLI exception, the gate's record validation against real files (missing, malformed, skipped, unbound), the marker render guard, and the per-arm step-cap arithmetic against the job bound. Run npm run test:scripts -- scripts/tests/qwen-autofix-workflow.test.js.
  • With the variable unset, expect behavior identical to today except a 190-minute step backstop that the unchanged 120-minute agent budget never reaches.
  • What only a real armed round can prove (named in af-156): the nested review resolves model credentials inside the sandbox, finds the CLI bundle at the workspace path, takes the direct execution path there, and finishes a ~300-line delta in 45–60 minutes. Suggested rollout: set QWEN_AUTOFIX_SELF_REVIEW=on for one PR you are watching, read the gate advisory and the marker on its round report, then switch to ab.

Evidence (Before & After)

N/A (workflow, gate script, skill prose, tests).

Tested on

OS Status
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux

Environment (optional)

Linux, npm run test:scripts on the workflow test file: 249/250 pass. The one failure (locks the runner file-command backing files against env plants) fails identically on main on this root-run machine — a chmod lock does not bind root — and is unrelated to this change.

Risk & Scope

  • Main risk or tradeoff: an armed round skips the same-run repair. The 345-minute job bound cannot hold the 190-minute armed step and the 70+60 repair chain, and a GitHub-hosted job is cut at 360 regardless; the repair fired on 7 of 79 acted rounds (9%), and a deterministic rejection then stays retryable for the next round exactly as it did before the repair existed. The A/B must read gate rejections per arm beside the round counts.
  • Not validated / out of scope: no armed round has run yet — the sandbox-side facts above are the first thing to verify. The record is agent-authored except for its binding and grammar. Token cost is not measured (wall-clock is). More than one pass per round is deliberately not offered: the 360-minute cap admits at most two, and there is no convergence data for a second one.
  • Breaking changes / migration notes: none; the default off changes nothing. A step-level timeout-minutes now carries an expression (job-level ones already do in this repository).

Linked Issues

None. Design record: af-156 in the workflow design doc.

中文说明

这个 PR 做了什么

给 autofix 的 address-review 通道加一个可选、按 A/B 度量的轮内自审。一轮被 arm 时,它在可信检查通过之后、提交之前,对自己的 delta 做一次有界的对抗式评审(与本地 /autofix 模式相同的 review run,high effort),把这次评审发现的问题修掉再推送。随后该轮把发生了什么写进一个机器可读的文件;验证门校验该记录、把它绑定到即将推送的提交的 tree id,轮报告带上 <!-- autofix-self-review … --> marker,实验结果可以直接从 PR 线程读取。不做任何拒绝:门只做度量。

臂由仓库变量 QWEN_AUTOFIX_SELF_REVIEW 决定——off(默认)、ab(奇数 PR 号 arm、偶数为对照臂)或 on——每轮在 prepare 里解析一次,GitHub 托管 runner 上永不 arm。armed 的轮获得 180 分钟 agent 预算、按臂区分的步骤上限(armed 190、非 armed 130),跳过同轮 repair 链;delta 少于 150 变更行或剩余时间不足 75 分钟时跳过这次评审本身。skill 只从 Invocation 块的三行(臂、CLI 入口、本轮 deadline)读取信息,不看别的。

为什么需要

2026-09-10 对全部 40 个 autofix/takeover PR 的测量(79 个 acted 轮、1369 条行内发现,每条在其评审 head 上 blame 归因):一轮推送之后,下一次评审的新 Critical 有 73%、新 Suggestion 有 93% 落在该轮自己的 delta 上;这些评审里 54% 的全部新发现都在 delta 上,critical floor 下 62% 会变成空评审。每个推送轮花费整周期中位约 870 分钟(agent 118、评审周转 289、到下一轮的空闲 348)。因此推送前对 delta 做一次新鲜评审,作用域是对的,收益上限就是这 54–62% 的轮次。

同一份测量也钉死了形态。bot delta 上每 100 变更行 0.53 个 Critical,人类 delta 是 0.52——无论谁推送,每次评审约 2 个新 Critical,逐轮不衰减。评审员对新鲜 delta 的产出率才是不变量,不是修复者的代码,所以"审到干净"在一轮之内无法收敛;它只会把 churn 搬进一个有硬预算的步骤,超时又会喂给失败熔断器。因此:一次、有界、可度量。

评审验证计划

如何验证

  • 阅读工作流设计文档中的 af-156 条目:机制、步骤上限背后的算术、以及有意保留的限制。
  • scripts 测试文件覆盖:臂的解析(奇偶、on/off、托管 runner 永不 arm)、runner 打印的三行 Invocation 及其拒绝的取值、skill 的单次规则与 CLI 例外、门对真实文件的记录校验(缺失、畸形、跳过、未绑定)、marker 渲染守卫、以及按臂的步骤上限与 job 上限的算术。运行 npm run test:scripts -- scripts/tests/qwen-autofix-workflow.test.js
  • 变量未设置时,行为与今天完全一致,只是步骤后备上限变为 190 分钟——未变的 120 分钟 agent 预算永远碰不到它。
  • 只有真实 armed 轮才能证明的事(af-156 已点名):嵌套评审在沙箱内能拿到模型凭据、能在 workspace 路径找到 CLI bundle、在那里走直接执行路径、约 300 行的 delta 在 45–60 分钟内完成。建议的上线方式:先给一个你正在盯的 PR 设 QWEN_AUTOFIX_SELF_REVIEW=on,读其轮报告上的门 advisory 与 marker,再切到 ab

证据(前后对比)

N/A(工作流、门脚本、skill 文本、测试)。

测试平台

OS Status
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux

环境(可选)

Linux,对工作流测试文件运行 npm run test:scripts:249/250 通过。唯一失败的 locks the runner file-command backing files against env plants 在本机(root 运行)的 main 上同样失败——chmod 锁对 root 不生效——与本改动无关。

风险与范围

  • 主要风险或取舍:armed 的轮跳过同轮 repair。345 分钟的 job 上限容不下 190 分钟的 armed 步骤加 70+60 的 repair 链,而 GitHub 托管 job 无论如何在 360 分钟被切断;样本中 repair 在 79 个 acted 轮里触发 7 次(9%),此时确定性拒绝会留给下一轮重试,和 repair 存在之前完全一样。A/B 分析必须在轮数之外按臂读取门拒绝数。
  • 未验证 / 超出范围:还没有任何 armed 轮跑过——上面的沙箱侧事实是第一件要验证的事。记录除绑定与语法外由 agent 撰写。未测 token 成本(测了墙钟)。有意不提供每轮多次评审:360 分钟上限最多容纳两次,且第二次没有任何收敛数据。
  • 破坏性变更 / 迁移说明:无;默认 off 不改变任何行为。一个步骤级 timeout-minutes 现在带表达式(本仓库 job 级早已如此)。

关联 Issue

无。设计记录:工作流设计文档 af-156。

…ommit (A/B)

Measured on the takeover fleet (40 PRs, 79 acted rounds, 1369 inline
findings blamed at their review heads): after a round pushes, 73% of the
next review's new Criticals and 93% of its Suggestions sit on that
round's own delta, while Critical density is the same on bot and human
deltas (~2 per review, no decay across rounds). So a fresh adversarial
pass over the delta before the push has the right scope, and ONE bounded
pass, never a loop, is the right shape.

Armed by the QWEN_AUTOFIX_SELF_REVIEW repo variable (off|ab|on; ab splits
on PR parity), resolved once in prepare and never on a github-hosted
runner. An armed round gets a 180m agent budget under a per-arm step cap
(190/130), skips the same-run repair chain so the 345-minute job bound
still holds, and skips the pass below 150 changed lines or 75 minutes
left. The skill runs the review inside the session's own sandbox and
records self-review.json after its commit; the gate validates the record,
binds it to the pushed commit's tree id and publishes one token string
that the round report renders as an autofix-self-review marker. Advisory
only: nothing rejects.

Design record: qwen-autofix.md#af-156.
@wenshao
wenshao requested a lite review from Copilot September 10, 2026 02:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finished — CI landed green on 675c335 and the deferred approval was posted. finalize run

Qwen Triage 已完成 —— 675c335 的 CI 全绿,延迟审批已提交。查看 finalize 运行

The self-review arm added 4,978 bytes to qwen-autofix.yml, past the
4,096-byte ratchet, with 3,113 of them comment prose that af-156 already
carries. Compress those comments to pointers; the mechanism is unchanged.
@wenshao
wenshao requested a lite review from Copilot September 10, 2026 02:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — the measurement behind this is unusually concrete, which makes it easy to review.

Template — complete ✓ (all headings, Risk & Scope filled in, bilingual body).

Problem — observed, not theoretical. The claim is a fleet measurement (40 takeover PRs, 79 acted rounds, 1369 inline findings blamed at their review head) rather than a hypothesis: 73% of the next review's new Criticals land on the round's own delta, and a pushed round costs ~870 min median end to end. That is the right kind of evidence for a workflow change. One honest caveat — the measurement is not reproducible from anything in this PR (no script, no artifact, no query), so I am taking the numbers on your word. That is fine for a maintainer-authored, default-off experiment, but the A/B is only worth running if the baseline numbers can be re-derived later.

Direction — this is the repo's own autofix machinery, not the shipped product, so the ecosystem-CHANGELOG signal does not apply. The design record follows the established af-NNN convention in the workflow's sibling .md (af-156), and the mechanism is advisory-only behind a repo variable defaulting to off, so it cannot change behaviour for anyone until you turn it on. No direction concerns.

Size — no core paths touched (packages/** is untouched), so the core-module gate does not apply. Breakdown: 337 production lines (workflow yml 71, design doc 79, skill prose 93, gate script 52, report script 12, runner 30) vs 362 test lines. Under both the 500 and 1000 thresholds.

Separately, and this one is worth your attention: the first commit tripped the workflow-size ratchet (qwen-autofix.yml at 468,068 bytes against a recorded 463,090 + 4,096 allowance), and 675c335 fixed it by condensing comments rather than moving shell out. At the reviewed head the file is 466,737 bytes — 449 bytes under its ratchet, 3,263 under the repo's 470,000-byte gate, and 91% of GitHub's 512,000-byte start-runs limit. The gate's own header records what happens past that line: the loop went dark for a day on 2026-08-19. So this PR lands with essentially no headroom left, and the next change to this workflow inherits the wall. Moving the arm-resolution shell into .github/scripts/ would buy back real room; bumping .size-baseline with a stated reason is the other honest option. Non-blocking for this PR — it passes — but the file is now the constraint.

Approach — the scope feels right, and the one-pass reasoning is the strongest part: critical density is 0.53 per 100 changed lines on bot deltas against 0.52 on human ones, so a fresh reviewer yields ~2 Criticals per delta regardless of who wrote it. That genuinely rules out "review until clean" inside a bounded budget, and I would have reached the same shape. Three questions before the code review:

  • Does ab need to land in this PR? Your own suggested rollout is on for one watched PR, then ab. Dropping the parity split would remove the arm-resolution case branch and let the mechanism prove itself before the control arm exists. Not a blocker — a controlled comparison is the point of the exercise — but it is the one piece that could have been a follow-up.
  • The record is published only on the gate's outcome=fixed path, so the marker only reaches pushed and no-action rounds. Your Risk section says the A/B must read gate rejections per arm beside the round counts — how do you intend to read the arm off a rejected round? It is recomputable from the variable plus PR parity plus runner environment, but it will not be on the thread.
  • Related: the marker is rendered from both report shapes, but the no-action shape can never receive a non-empty record (the gate publishes self_review only after outcome=fixed). That call site looks unreachable — detail in the code review.

Risk — no elevated risk signals: none of the changed files match the revert-correlated path set, and this is same-repo (not a fork), feat-typed, so the fork-refactor approval guardrail does not apply.

Moving on to code review. 🔍

中文说明

感谢贡献——这个 PR 背后的度量非常具体,评审起来很容易。

模板 —— 完整 ✓(所有小标题齐全,Risk & Scope 有实际内容,正文双语)。

问题 —— 是已观测到的,不是理论性的。依据是一次车队级度量(40 个 takeover PR、79 个 acted 轮、1369 条行内发现,每条在其评审 head 上 blame 归因),而不是假设:下一次评审的新 Critical 有 73% 落在该轮自己的 delta 上,一个推送轮的端到端中位成本约 870 分钟。对于工作流改动来说这是正确的证据类型。一个诚实的保留意见——这份度量无法从 PR 里的任何东西复现(没有脚本、没有产物、没有查询语句),所以这些数字我只能采信你的说明。对于一个由维护者提交、默认关闭的实验来说这没问题,但 A/B 只有在基线数字日后能重新推导出来时才值得跑。

方向 —— 这是仓库自身的 autofix 机制,不是发布的产品,所以生态 CHANGELOG 信号不适用。设计记录沿用了工作流同级 .md 里既有的 af-NNN 约定(af-156),并且机制是纯建议性的、由一个默认为 off 的仓库变量控制,因此在你打开之前不会改变任何人的行为。方向上没有顾虑。

规模 —— 未触及核心路径(packages/** 完全没动),所以核心模块门不适用。拆分:生产代码 337 行(工作流 yml 71、设计文档 79、skill 文本 93、门脚本 52、报告脚本 12、runner 30)对 测试 362 行。低于 500 与 1000 两个阈值。

另有一点值得你注意:第一个提交触发了工作流体积棘轮(qwen-autofix.yml 468,068 字节,记录值 463,090 + 4,096 余量),675c335 是靠压缩注释而不是把 shell 移出去修好的。在本次评审的 head 上该文件是 466,737 字节——比棘轮上限少 449 字节,比仓库 470,000 字节的门少 3,263 字节,并且已达 GitHub 512,000 字节"不再启动运行"上限的 91%。门脚本自己的注释记录了越过那条线会发生什么:2026-08-19 整个循环静默了一天。所以这个 PR 落地后几乎没有余量,下一个改动这个工作流的 PR 会直接撞墙。把臂解析的 shell 移到 .github/scripts/ 能换回真实空间;另一个诚实的选项是带上理由提高 .size-baseline。对本 PR 不构成阻塞——它是通过的——但这个文件现在已经成了约束条件。

方案 —— 范围合理,而"只做一次"的论证是最有力的部分:bot delta 上每 100 变更行 0.53 个 Critical,人类 delta 是 0.52,所以无论谁写的,新鲜评审员在每个 delta 上都会产出约 2 个 Critical。这确实排除了在有硬预算的轮内"审到干净",我自己也会得出同样的形态。进入代码审查前有三个问题:

  • ab 需要在这个 PR 里落地吗?你自己建议的上线路径是先对单个盯着的 PR 设 on,再切 ab。去掉奇偶分流就能移除臂解析里的 case 分支,让机制先自证再引入对照臂。不是阻塞项——受控对比正是这件事的意义——但这是唯一可以作为后续 PR 的部分。
  • 记录只在门的 outcome=fixed 路径上发布,所以 marker 只会出现在推送轮和无操作轮。你的 Risk 一节说 A/B 必须在轮数之外按臂读取门拒绝数——那么被拒绝的轮次,你打算从哪里读出它属于哪个臂?它可以由变量加 PR 奇偶加 runner 环境重新算出来,但不会出现在 PR 线程上。
  • 相关的一点:marker 在两种报告形态里都会渲染,但无操作形态永远拿不到非空记录(门只在 outcome=fixed 之后发布 self_review)。那个调用点看起来不可达——细节见代码审查。

风险 —— 无升级风险信号:改动文件都不匹配与回滚相关的路径集合,并且这是同仓库(非 fork)、feat 类型,所以 fork-refactor 审批护栏不适用。

进入代码审查 🔍

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 675c3355dfd5cbe309b34b46ea31907df1d5e0e9 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Code review

I read the title and the "Why it's needed" section first and wrote down what I would have done before opening the diff: one pre-commit review pass in the address-review skill, gated by a repo variable, bounded by a wait cap and a small-delta skip, recorded in a machine-readable file that the gate validates and renders as an HTML marker on the round report. That is what this is. The one place I thought I had a simpler shape — keep the existing 120-minute agent budget and let the pass eat into it, which would drop the per-arm step cap, the budget-clamp branch, the repair-chain skip and the whole job-bound re-arithmetic — does not survive contact with your own skip rule: the pass is skipped when fewer than 75 minutes remain, so under a 120-minute budget it could only start in the first 45 minutes and would come back skipped-deadline on most rounds. The A/B would have measured nothing. So the 180-minute armed budget is load-bearing, and the timeout machinery that follows from it is not scope creep.

The security handling is the part I looked hardest at, since this is CI machinery that executes PR code next to a write PAT, and it holds up:

  • The arm comes from a repo variable, which a PR author cannot set, and PR is guarded by ^[0-9]+$ before it reaches arithmetic, so the parity split cannot be talked into arming.
  • The two values that land verbatim in the agent prompt are pinned in run-agent.mjs — the CLI entry to a path charset, the deadline to an ISO instant or empty. A repo variable cannot smuggle prose into the skill. Worth noting you did not make the CLI entry variable-driven; it is a workflow literal, which is the safer choice.
  • The record is agent-authored and branch-writable, so it is grammar-pinned twice: once in the gate against a tight arm=on status=… minutes=N shape, and again at the render site against [a-z0-9=. -]+, which excludes > so a forged value cannot close the marker early. The test pins arm=on --> <script> rendering to nothing.
  • The gate is advisory-only — no reject_fix on this path — so a malformed record can never block a push. Right call for a measurement.
  • self-review.json was added to the artifact dump inside the loop that neutralizes both workflow-command syntaxes, so an agent-written file cannot forge a ::error:: or ##[group] in the job log.
  • QWEN_REVIEW_SANDBOX=off normally deserves scrutiny, but here it avoids nesting a container inside the operator's container rather than removing a boundary. The outer sandbox is untouched.

I also checked whether the new marker perturbs the scan's bot-comment filters (BOT_COMMENT_FILTER and the inline <!-- (autofix-eval|autofix-rearm|qwen-triage|…) tests). It does not: autofix-self-review is not in any of those lists, and adding a marker cannot move a comment from excluded to included. This matches how the existing autofix-growth-now and autofix-growth-audit markers are already handled.

Four suggestions, none blocking.

1. The no-action render call site can never produce output. emit_self_review_marker is called from both report shapes, but the gate publishes self_review only after echo "outcome=fixed", and every other exit — reject_fix, outcome=failed, outcome=noop, the handoff paths — returns before that line. Finalize then only re-publishes a non-empty value, and its clearing branch is the narrow forged-or-crashed-gate case. Walking the repair interleaving: a non-empty record with outcome != fixed would need retryable=true on a gate that also reached outcome=fixed, and reject_fix sets outcome=failed before it sets retryable, so those are mutually exclusive. The second call site is therefore output-less, and expect(pushAndReport.match(/^\s*emit_self_review_marker$/gm)).toHaveLength(2) pins it, so a future cleanup fails a test. Either drop the second call and the length assertion, or publish the record on the noop path too — the latter would make no-action rounds measurable, which may be worth having for the A/B denominator.

2. An unrelated comment block is now orphaned above the new function. In autofix-push-and-report.sh, the pre-existing four-line "Growth-audit trail (+ re-arm on sound)…dead-key hazard" preamble documented emit_growth_audit_marker. The new function was inserted between that preamble and its function, while the # Full rationale → qwen-autofix.md#af-131 line stayed behind with the growth marker — so the af-131 dead-key rationale now reads as if it documents the self-review marker, and the growth function's own comment is split in two. Moving emit_self_review_marker below emit_growth_audit_marker fixes it without touching either comment.

3. The default-path claim in the PR body does not match the code. "With the variable unset, expect behavior identical to today except a 190-minute step backstop that the unchanged 120-minute agent budget never reaches" — with the variable unset the arm resolves to off, so timeout-minutes evaluates to 130, exactly as today. The 190-minute cap only exists when armed, and arming also raises the budget to 180m, so the described state (190 backstop beside an unchanged 120m budget) cannot occur. And the default path is not byte-identical to today: the gate sets SELF_REVIEW_RECORD='arm=off' before the arm check and publishes it unconditionally on the fixed path, so every pushed round now gains <!-- autofix-self-review arm=off --> in its report. I assume that is deliberate — it is how the ab control arm gets identified on even-numbered PRs — but it is a real change on the default path and the body currently says the opposite. Worth one corrected sentence, since a reviewer gauging blast radius from that line would under-count it.

4. Nothing pins that the prepare step can actually see PR. The parity split reads ${PR}, which reaches the prepare step from the job-level env: PR: '${{ matrix.target.pr }}' — I confirmed that wiring is present. But the arm-replay test injects PR into the child environment itself, and no test pins the job-level env.PR or the address step's PR: '${{ env.PR }}' (the only matrix.target.pr assertions in the file cover the concurrency group key and WORKDIR). So if that env were ever moved to per-step declarations, the ab arm would silently stop arming — the numeric guard fails closed to off — while the whole suite stayed green. This is the dead-switch shape rather than a live defect: the blast radius is bounded, because a PR that vanished entirely would break --pr loudly in the same step. One assertion on the job-level line would close it.

One thing to keep on your radar rather than fix here: the second commit got under the ratchet by condensing comments, which leaves qwen-autofix.yml at 466,737 bytes — 449 bytes under its ratchet limit, 3,263 under the repo's 470,000-byte gate, and 91% of GitHub's 512,000-byte start-runs limit, so the "approaching the gate" warning still fires. The gate's own header records what crossing 512,000 does: the loop went dark for a day on 2026-08-19. The design prose correctly went into the sibling .md, but the arm-resolution shell is still inline in the prepare step and is the obvious next thing to move into .github/scripts/ if you want real headroom back.

sequenceDiagram
    participant P1 as Prepare step
    participant P2 as run-agent.mjs
    participant P3 as Autofix skill agent
    participant P4 as Nested review run
    participant P5 as Verification gate
    participant P6 as Finalize verification
    participant P7 as Push and report
    P1->>P1: resolve arm from repo variable and PR parity
    P1->>P2: arm, CLI entry, round deadline
    P2->>P3: three Invocation lines
    P3->>P4: one bounded delta review at high effort
    P4-->>P3: findings
    P3->>P3: apply safe acts, re-run checks, commit once
    P3->>P5: self-review.json carrying the tree id
    P5->>P5: validate grammar, bind tree to the pushed head
    P5->>P6: one token string, advisory only
    P6->>P7: record selected with the outcome
    P7->>P7: render the autofix-self-review marker
Loading
Files changed (7 of 7 shown)
File What changed
.github/workflows/qwen-autofix.yml Two new workflow variables, the arm resolution in prepare, the per-arm step cap and budget clamp, the deadline and CLI plumbing into the runner, the repair-chain skip, and the record's trip through Finalize into the report step.
.github/scripts/run-autofix-review-verification.sh Validates the agent-written record's shape, binds its tree id to the head about to be pushed, appends a gate advisory, and publishes one token string on the fixed path only.
.github/scripts/autofix-push-and-report.sh New render function with a charset guard, called from both report shapes.
.qwen/skills/autofix/SKILL.md The one-pass rule, the skip conditions, the exact review command, the record schema, and the single carved-out CLI exception to the do-not-run-the-CLI rule.
.qwen/skills/autofix/scripts/run-agent.mjs Three new flags, charset and ISO-instant validation on the two that reach the prompt, and the three Invocation lines.
.github/workflows/qwen-autofix.md Design entry af-156 — the measurement, the one-pass argument, the mechanism, and the deliberate limits.
scripts/tests/qwen-autofix-workflow.test.js Arm-resolution parity, Invocation lines and refusals, the skill's one-pass prose, gate record validation against real files, the render guard, and the per-arm step-cap arithmetic against the job bound.

Testing

This was an unattended CI run, so per the skill's rules I did not build or execute anything from this PR — all evidence below is the PR's own CI, read through the API.

The head moved mid-review. At the first commit ecb83d9 the Lint & Static job failed at step 7 of 35 (Check workflow file size): .github/workflows/qwen-autofix.yml was 468,068 bytes against a recorded 463,090 with a 4,096 allowance, so 882 bytes over. Because that gate sits early in the job, steps 8–35 were all skipped — ESLint, actionlint, shellcheck, yamllint, Prettier, the i18n check, the settings-schema check and the .github/scripts helper tests never ran, and Test (macos-latest) and Test (windows-latest) were skipped. So at that commit the PR had no static-analysis signal at all, which mattered here specifically because actionlint is what would have validated the new step-level timeout-minutes expression.

675c335 ("ci(autofix): keep the workflow under its size allowance") is a comment-only condensation — I diffed the two commits and every functional line is byte-identical; the arm block, the timeout-minutes expression, BUDGET_CAP_MS=10800000, ROUND_DEADLINE, the repair if: and the Finalize/report env are all unchanged. It brought the file to 466,737 bytes, under the ratchet, and Lint & Static is now green, which means actionlint, shellcheck and the helper tests all ran and passed.

Final CI results for 675c335 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Classify PR ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Integration Tests (no-AK, No Sandbox) ✅ success
Lint & Static (ubuntu-latest, Node 22.x) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

No check on the reviewed head is red. The macOS/Windows unit jobs and the CLI integration job are skipped by the platform-sensitivity classification, which is the expected outcome for a PR that touches only workflow YAML, CI shell, skill prose and a scripts test — there is no platform-dependent product code here. Test (ubuntu-latest) was still running when I fetched this once; I did not poll, and I am not guessing its result. That job is what runs scripts/tests/qwen-autofix-workflow.test.js, so the 353 new test lines are not yet confirmed by CI.

Not verified, and why no sandboxed lane can close it: the central claim — that one bounded delta review before the push reduces what the next review finds — is not verifiable from the diff, and neither isolated lane applies. @qwen-code /verify does A/B load-bearing proof of PR code against the base build, but this PR changes no shipped product code, so there is nothing for that comparison to bear on; @qwen-code /tmux drives the TUI, and there is no TUI surface. What would actually settle it is the rollout you already describe in af-156: set QWEN_AUTOFIX_SELF_REVIEW=on for one PR you are watching and read the gate advisory and the marker off its round report. I am naming that explicitly rather than suggesting a lane that cannot run, because the specific unknowns are runner facts — whether the nested review run resolves model credentials inside the sandbox and whether ${GITHUB_WORKSPACE}/dist/cli.js resolves at that same path inside it. On the second one I can narrow your uncertainty from the workflow: the bundle does exist on the host (Restore CLI bundle asserts test -f dist/cli.js) and the generated qwen wrapper is literally exec node "${GITHUB_WORKSPACE}/dist/cli.js" "$@", so the entry you pass matches the wrapper exactly. Whether that path survives into the sandbox is still the first armed round's to prove. The failure mode is soft — review-failed, push regardless — so a miss costs a wasted wait cap, not a broken round.

Also unverified: your local npm run test:scripts result of 249/250 is your claim, not evidence I re-ran, and I did not independently confirm that the one failure (locks the runner file-command backing files against env plants) also fails on main under root. Your explanation — a chmod lock does not bind root — is plausible and the test name is unrelated to this change, but it stays attributed to you.

中文说明

代码审查

我先只读了标题和"为什么需要",在看 diff 之前写下了自己的方案:在 address-review skill 里加一次提交前评审,由仓库变量控制,用等待上限和小 delta 跳过加以约束,把结果写进一个机器可读文件,由门校验并渲染成轮报告上的 HTML marker。这个 PR 就是这么做的。唯一一处我以为自己找到了更简形态的地方——保留现有 120 分钟 agent 预算、让这次评审从中扣——撑不过你自己的跳过规则:剩余不足 75 分钟就跳过,所以在 120 分钟预算下它只能在前 45 分钟内启动,多数轮次都会返回 skipped-deadline,A/B 什么也测不到。因此 180 分钟的 armed 预算是承重的,随之而来的超时机制不是范围蔓延。

安全处理是我看得最仔细的部分,因为这是在执行 PR 代码、旁边放着写权限 PAT 的 CI 机制,而它站得住:

  • 臂来自仓库变量,PR 作者无法设置;PR 在进入算术之前有 ^[0-9]+$ 守卫,所以奇偶分流无法被诱导 arm。
  • 两个会原样进入 agent prompt 的值在 run-agent.mjs 里被钉住——CLI 入口限制为路径字符集,deadline 限制为 ISO 时刻或空。仓库变量无法把文本走私进 skill。值得一提的是你没有把 CLI 入口做成变量驱动,它是工作流字面量,这是更安全的选择。
  • 记录由 agent 撰写、分支可写,所以语法被钉了两次:门里按严格的 arm=on status=… minutes=N 形状校验一次,渲染处再按 [a-z0-9=. -]+ 校验一次,后者排除了 >,因此伪造值无法提前闭合 marker。测试钉住了 arm=on --> <script> 渲染为空。
  • 门是纯建议性的——这条路径上没有 reject_fix——所以畸形记录永远无法阻止推送。对一次度量来说这是对的。
  • self-review.json 被加进了产物 dump 中那个会中和两种 workflow-command 语法的循环,所以 agent 写的文件无法在 job 日志里伪造 ::error::##[group]
  • QWEN_REVIEW_SANDBOX=off 通常值得警惕,但这里是避免在运维方的容器里再套一层容器,而不是移除边界。外层沙箱没有被动。

我还检查了新 marker 是否会干扰扫描的 bot 评论过滤器(BOT_COMMENT_FILTER 以及内联的 <!-- (autofix-eval|autofix-rearm|qwen-triage|…) 判断)。不会:autofix-self-review 不在任何一份列表里,而新增一个 marker 不可能把评论从"已排除"变成"已包含"。这与现有 autofix-growth-nowautofix-growth-audit marker 的处理方式一致。

四条建议,均不阻塞。

1. 无操作形态的那个渲染调用点永远不会有输出。 emit_self_review_marker 在两种报告形态里都被调用,但门只在 echo "outcome=fixed" 之后发布 self_review,其他所有出口——reject_fixoutcome=failedoutcome=noop、各 handoff 路径——都在那一行之前返回。Finalize 又只重新发布非空值,其清空分支仅限于"门被伪造或崩溃"这一狭窄情形。把 repair 的交错情况走一遍:要在 outcome != fixed 时拿到非空记录,需要一个既设了 retryable=true 又到达 outcome=fixed 的门,而 reject_fix 是先写 outcome=failed 再写 retryable,两者互斥。所以第二个调用点没有输出,而 expect(pushAndReport.match(/^\s*emit_self_review_marker$/gm)).toHaveLength(2) 把它钉住了,将来清理时会撞测试。要么删掉第二个调用和该断言,要么在 noop 路径上也发布记录——后者能让无操作轮变得可度量,对 A/B 的分母也许正好有用。

2. 一段无关的注释现在被孤悬在新函数上方。autofix-push-and-report.sh 里,原有的四行"Growth-audit trail (+ re-arm on sound)…dead-key hazard"前言是 emit_growth_audit_marker 的文档。新函数被插在这段前言和它的函数之间,而 # Full rationale → qwen-autofix.md#af-131 那行留在了 growth marker 旁边——于是 af-131 的 dead-key 论证读起来像是在描述自审 marker,而 growth 函数自己的注释被劈成了两半。把 emit_self_review_marker 移到 emit_growth_audit_marker 下面即可修好,两段注释都不用动。

3. PR 正文关于默认路径的说法与代码不符。"变量未设置时,行为与今天完全一致,只是步骤后备上限变为 190 分钟——未变的 120 分钟 agent 预算永远碰不到它"——变量未设置时臂解析为 offtimeout-minutes 求值为 130,与今天完全相同。190 分钟上限只在 armed 时存在,而 arm 同时会把预算抬到 180 分钟,所以所描述的状态(190 后备上限配未变的 120 分钟预算)不可能出现。而且默认路径并非与今天逐字节一致:门在臂判断之前就设了 SELF_REVIEW_RECORD='arm=off',并在 fixed 路径上无条件发布,所以每个推送轮的报告现在都会多一行 <!-- autofix-self-review arm=off -->。我猜这是有意的——它正是 ab 模式下偶数 PR 对照臂的标识方式——但它确实是默认路径上的真实改动,而正文目前说的是相反的话。值得改一句话,因为按那行判断影响面的评审者会低估它。

4. 没有任何测试钉住 prepare 步骤能否真的看到 PR 奇偶分流读 ${PR},它经由 job 级 env: PR: '${{ matrix.target.pr }}' 到达 prepare 步骤——我确认了这条接线存在。但臂重放测试是自己把 PR 注入子环境的,而没有测试钉住 job 级 env.PR 或 address 步骤的 PR: '${{ env.PR }}'(文件里仅有的 matrix.target.pr 断言覆盖的是并发组 key 和 WORKDIR)。所以如果那个 env 哪天被改成按步骤声明,ab 臂会静默不再 arm——数字守卫向 off 失败关闭——而整个测试套件依然是绿的。这属于"死开关"形态而非现存缺陷:影响面有界,因为 PR 若整体消失会在同一步骤里把 --pr 明显地打断。加一条对 job 级那行的断言就能封住。

有一件事请你留意、但不必在此修:第二个提交是靠压缩注释挤到棘轮之下的,这让 qwen-autofix.yml 停在 466,737 字节——距棘轮上限 449 字节,距仓库 470,000 字节的门 3,263 字节,已达 GitHub 512,000 字节"不再启动运行"上限的 91%,所以"接近门限"的警告仍会触发。门脚本自己的注释记录了越过 512,000 的后果:2026-08-19 整个循环静默了一天。设计文本已正确进入同级 .md,但臂解析的 shell 仍内联在 prepare 步骤里,如果你想换回真实余量,它是下一个该移进 .github/scripts/ 的东西。

测试

这是一次无人值守的 CI 运行,所以按 skill 规则我没有构建或执行本 PR 的任何东西——下面所有证据都是通过 API 读取的本 PR 自己的 CI。

head 在评审过程中变了。在第一个提交 ecb83d9 上,Lint & Static 作业在第 35 步中的第 7 步失败Check workflow file size):.github/workflows/qwen-autofix.yml 为 468,068 字节,记录值 463,090、余量 4,096,超出 882 字节。由于该门位于作业前部,第 8–35 步全部跳过——ESLint、actionlint、shellcheck、yamllint、Prettier、i18n 检查、settings-schema 检查以及 .github/scripts 辅助测试都没跑,Test (macos-latest)Test (windows-latest) 也被跳过。所以在该提交上这个 PR 完全没有静态分析信号,而这在这里尤其要紧,因为验证新的步骤级 timeout-minutes 表达式正是 actionlint 的活。

675c335("ci(autofix): keep the workflow under its size allowance")是纯注释压缩——我对比了两个提交,每一行功能代码都逐字节相同;臂解析块、timeout-minutes 表达式、BUDGET_CAP_MS=10800000ROUND_DEADLINE、repair 的 if: 以及 Finalize/报告的 env 全都没变。它把文件降到 466,737 字节,进入棘轮之下,Lint & Static 现在是绿的,意味着 actionlint、shellcheck 和辅助测试都跑过并通过了。

被评审 head 上没有任何红色检查。macOS/Windows 单测作业与 CLI 集成作业是被平台敏感性分类跳过的,对于一个只改工作流 YAML、CI shell、skill 文本和 scripts 测试的 PR 来说这是预期结果——这里没有平台相关的产品代码。我一次性抓取时 Test (ubuntu-latest) 仍在运行;我没有轮询,也不猜它的结果。那个作业正是运行 scripts/tests/qwen-autofix-workflow.test.js 的,所以 353 行新测试尚未被 CI 确认。

**未验证,以及为何没有沙箱通道能补上:**核心主张——推送前一次有界的 delta 评审能减少下一次评审的发现——无法从 diff 验证,而两条隔离通道都不适用。@qwen-code /verify 做的是 PR 代码对基线构建的 A/B 承重证明,但本 PR 没有改动任何发布的产品代码,所以那种对比无从承重;@qwen-code /tmux 驱动 TUI,而这里没有 TUI 面。真正能定论的是你在 af-156 里已经描述的上线方式:对你正在盯的一个 PR 设 QWEN_AUTOFIX_SELF_REVIEW=on,从其轮报告上读门 advisory 与 marker。我明确点出这一点,而不是建议一条跑不起来的通道,因为具体的未知项是 runner 事实——嵌套的 review run 能否在沙箱内解析模型凭据,以及 ${GITHUB_WORKSPACE}/dist/cli.js 在沙箱内是否仍解析到同一路径。第二点我可以从工作流里收窄你的不确定性:bundle 在宿主上确实存在(Restore CLI bundle 断言了 test -f dist/cli.js),生成的 qwen 包装脚本字面上就是 exec node "${GITHUB_WORKSPACE}/dist/cli.js" "$@",所以你传入的入口与包装脚本完全一致。该路径能否延续进沙箱,仍要由第一个 armed 轮来证明。失败模式是软的——review-failed,照常推送——所以即便落空,代价是浪费一次等待上限,而不是弄坏一轮。

同样未验证:你本地 npm run test:scripts 的 249/250 是你的说法,不是我重跑的证据;我也没有独立确认那唯一失败项(locks the runner file-command backing files against env plants)在 root 下的 main 上同样失败。你的解释——chmod 锁对 root 不生效——合理,且该测试名与本次改动无关,但这仍归属于你。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 675c3355dfd5cbe309b34b46ea31907df1d5e0e9 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — solid design and careful security handling; four non-blocking suggestions, and the one real reservation is about the file this lands in rather than the change itself.

Going back to the proposal I wrote before reading the diff: this matches it, and it beat the one simplification I thought I had. I assumed the armed round could live inside the existing 120-minute agent budget, which would have removed the per-arm step cap, the budget-clamp branch, the repair-chain skip and the job-bound re-arithmetic — roughly half the workflow diff. It cannot, for a reason your own skip rule supplies: the pass is skipped below 75 minutes remaining, so a 120-minute budget confines it to the first 45 minutes and it would return skipped-deadline on most rounds. The A/B would have measured nothing. Once the 180-minute budget is forced, the timeout arithmetic that follows is forced with it, and the test that derives both round shapes from the workflow and checks them against the 345-minute job bound is the right way to keep that from rotting.

The strongest thing here is not the mechanism, it is the argument that fixes its shape. Critical density of 0.53 per 100 changed lines on bot deltas against 0.52 on human ones says the reviewer's yield on a fresh delta is the invariant, not the fixer's code quality — so "review until clean" cannot converge inside a round, it just relocates the churn into a step with a hard budget whose overrun feeds the failure breakers. That is a non-obvious observation and it is what makes "one pass, bounded, measured" the correct answer rather than a compromise. I would not have thought to go and measure density on human deltas to settle the loop question.

What I could not verify, plainly: no armed round has ever run, so the central behavioural claim rests on your rollout plan rather than on evidence. I am comfortable approving anyway, because the default is off and nothing changes for anyone until you set the variable — which is exactly how an unvalidated experiment should land. If the first armed round discovers the nested review cannot resolve credentials inside the sandbox, the cost is 337 dormant lines and a marker on round reports, not a broken loop. The related measurement caveat stands: the baseline numbers (73%, 93%, 54–62%, 870 min) are not re-derivable from anything in this PR. The A/B marker is what makes the next numbers re-derivable, but the ones justifying the change are currently a claim.

On whether I would maintain this in six months: yes, with one irritation. The double grammar-pinning and the tree-id binding look like trying hard until you remember the record is agent-authored on a branch the agent can write, at which point they are the minimum. The irritation is that qwen-autofix.yml now sits 449 bytes under its ratchet and 3,263 under the repo gate, at 91% of the limit where GitHub stops starting runs silently — and the gate's own header records the loop going dark for a day when that happened in August. You got under by condensing comments, which is the weakest of the three remedies the gate offers. The arm-resolution shell is still inline in the prepare step and is the obvious next thing to move into .github/scripts/. Not this PR's job to fix, but the next workflow change inherits a wall, and it will not be this PR that gets blamed.

Am I being worn down by volume? Worth asking out loud, since you have 24 open PRs and this is one of a series reshaping the same review/autofix loop. I evaluated it on its own evidence and I am naming the suggestions rather than waving them through. One coordination note that falls out of the pattern rather than the diff: this PR skips the same-run repair chain on armed rounds, and the open brake/failure-machinery PRs in that series are the ones that decide what a skipped repair costs. The af-156 text already says the A/B must read gate rejections per arm beside the round counts — that instruction is the load-bearing part, and it is worth making sure whoever reads the experiment actually has per-arm rejection counts, which today they would have to reconstruct from the variable, PR parity and runner environment rather than read off the thread (the marker only lands on pushed rounds).

The four suggestions from the code review are all worth a pass but none should hold the merge: the output-less second render call site that a toHaveLength(2) assertion now pins, the af-131 comment preamble orphaned above the new function, the PR-body sentence describing a 190-minute backstop beside an unchanged 120-minute budget that cannot occur, and the missing assertion on the job-level PR the parity split depends on.

Approval is deferred until CI lands green on 675c3355dfd5cbe309b34b46ea31907df1d5e0e9. Test (ubuntu-latest, Node 22.x) was still running when I fetched the checks once — that is the job which runs scripts/tests/qwen-autofix-workflow.test.js, so the 353 new test lines are not yet confirmed. I did not poll and I am not approving against a result that does not exist yet. Everything else on the head is green or legitimately skipped, including Lint & Static, which means actionlint validated the new step-level timeout-minutes expression and shellcheck saw both scripts. If that last job lands green the approval follows automatically against this exact commit; if it lands red or the head moves, it will not.

中文说明

信心:4/5 —— 设计扎实、安全处理谨慎;四条不阻塞的建议,而唯一真正的保留意见是关于这个改动所落进的文件,而不是改动本身。

回到我在看 diff 之前写下的方案:这个 PR 与它一致,而且它战胜了我自以为找到的那个简化。我原以为 armed 轮可以待在现有的 120 分钟 agent 预算里,那样就能去掉按臂区分的步骤上限、预算 clamp 分支、repair 链跳过以及 job 上限的重新算术——大约是工作流 diff 的一半。做不到,而理由正是你自己的跳过规则给出的:剩余不足 75 分钟就跳过,所以 120 分钟预算把它限制在前 45 分钟内,多数轮次都会返回 skipped-deadline,A/B 什么也测不到。一旦 180 分钟预算成为必需,随之而来的超时算术也就成为必需;而那个从工作流里推导出两种轮次形态、再对着 345 分钟 job 上限校验的测试,正是防止它日后腐化的正确做法。

这里最有力的部分不是机制,而是钉死其形态的那个论证。bot delta 上每 100 变更行 0.53 个 Critical、人类 delta 上 0.52,这说明"评审员在新鲜 delta 上的产出率"才是不变量,而不是修复者的代码质量——所以"审到干净"在一轮之内无法收敛,它只是把 churn 搬进一个有硬预算、超时又会喂给失败熔断器的步骤。这是个不显然的观察,也正是它让"一次、有界、可度量"成为正确答案而非妥协。我想不到要去测人类 delta 上的密度来解决这个循环问题。

我无法验证的部分,直说:还没有任何 armed 轮跑过,所以核心的行为主张依赖的是你的上线计划,而不是证据。我仍然可以放心批准,因为默认是 off,在你设置变量之前对任何人都不改变行为——这正是一个未经验证的实验应有的落地方式。如果第一个 armed 轮发现嵌套评审无法在沙箱内解析凭据,代价是 337 行休眠代码加轮报告上的一个 marker,而不是一个坏掉的循环。相关的度量保留意见依然成立:基线数字(73%、93%、54–62%、870 分钟)无法从本 PR 里的任何东西重新推导出来。A/B marker 让下一批数字变得可重新推导,但支撑这个改动的那些数字目前仍是主张。

关于半年后我是否愿意维护它:愿意,但有一处不适。双重语法钉定与 tree id 绑定看起来像是用力过猛,直到你想起这份记录是 agent 撰写、而且写在 agent 自己能写的分支上——那一刻它们就是最低限度。不适之处在于 qwen-autofix.yml 现在距其棘轮上限只有 449 字节、距仓库门限 3,263 字节,已达 GitHub 静默停止启动运行的上限的 91%——而门脚本自己的注释记录了八月那次越线导致循环静默一整天。你是靠压缩注释挤到线下的,那是门给出的三个补救办法里最弱的一个。臂解析的 shell 仍内联在 prepare 步骤里,是下一个该移进 .github/scripts/ 的明显对象。这不是本 PR 该修的活,但下一个改动工作流的 PR 会继承一堵墙,而被怪罪的不一定是这个 PR。

我是不是被数量磨软了?值得大声问一句,因为你有 24 个开放 PR,而这是重塑同一套 review/autofix 循环的系列之一。我是按它自身的证据评审的,并且是把建议点名而不是挥手放过。有一条协调性备注是从这个系列模式(而非 diff)里掉出来的:本 PR 在 armed 轮跳过同轮 repair 链,而该系列中那些开放的熔断/失败机制 PR 正是决定"跳过一次 repair 代价几何"的地方。af-156 的文本已经说明 A/B 必须在轮数之外按臂读取门拒绝数——那句指示是承重的,而值得确保的是:真正读这个实验的人手上确实有按臂的拒绝数。今天他们必须从变量、PR 奇偶和 runner 环境重新算出来,而无法直接从线程上读到(marker 只落在推送轮上)。

代码审查里的四条建议都值得过一遍,但没有一条该压住合并:那个没有输出、却被 toHaveLength(2) 断言钉住的第二个渲染调用点;被孤悬在新函数上方的 af-131 注释前言;PR 正文里那句描述了"190 分钟后备上限配未变的 120 分钟预算"这一不可能状态的话;以及缺少对奇偶分流所依赖的 job 级 PR 的断言。

审批已推迟,等 CI 在 675c3355dfd5cbe309b34b46ea31907df1d5e0e9 上变绿。 我一次性抓取检查结果时 Test (ubuntu-latest, Node 22.x) 仍在运行——那正是运行 scripts/tests/qwen-autofix-workflow.test.js 的作业,所以 353 行新测试尚未被确认。我没有轮询,也不会对一个还不存在的结果给出批准。该 head 上其他一切都是绿的或合理跳过的,包括 Lint & Static——这意味着 actionlint 已验证新的步骤级 timeout-minutes 表达式,shellcheck 也看过两个脚本。如果最后那个作业变绿,批准会自动针对这个确切的提交跟上;如果它变红或 head 移动,则不会。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 675c3355dfd5cbe309b34b46ea31907df1d5e0e9 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship — CI landed green after the review. ✅

@wenshao

wenshao commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /review

@github-actions

Copy link
Copy Markdown
Contributor

Qwen Code review request accepted. Review is running in workflow run. A command-triggered review is not listed under the checks of this PR; the result is posted here as a review when it finishes.

@qqqys

qqqys commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Independent verification report — measured at head 675c3355dfd5

结论 / Conclusion: mergeable at the head named above. Every number below was computed from a fresh read taken immediately before this comment was posted; the live-state fields are head 675c3355dfd5, state=open merged=False, read at 2026-09-10T03:49:14Z.

This is an independent verification pass, not a re-statement of the bot's review. It ran because the gate conditions held at the time of the read: qwen-code-ci-bot APPROVED at head (2026-09-10T03:03:59Z, review 5162190969), CI 50/50 check-runs enumerated complete, 0 failing, histogram {'skipped': 36, 'success': 13, 'in_progress': 1}, and this pass found no Critical.


1. Instrument — and why it is not a tmux E2E

The standing convention for this report is a tmux-driven TUI run. That instrument is structurally inapplicable to this diff, and naming the substitute is the honest move rather than quietly skipping the section:

  • The arm only resolves on a self-hosted runner (RUNNER_ENVIRONMENT != 'github-hosted'), so no local run can enter the armed branch through the workflow.
  • The gate and report scripts are CI-plumbed bash: they read $GITHUB_OUTPUT, $WORKDIR, $GATE_LOG, $GITHUB_RUN_ID.
  • Nothing in the diff is reachable from a TUI surface a tmux driver can observe.

What is locally executable is the two places where this PR's own comments make load-bearing claims: run-agent.mjs's prompt assembly (which exposes --print-prompt, a side-effect-free oracle — it writes the prompt and process.exit(0) at :565-568, before any input-file check or agent spawn), and the gate's record-validation block. Both were lifted verbatim from the head blobs and executed. Blob identity, each verified by recomputing the git sha1 of the decoded content: run-agent.mjs head 95f4db76be07 (29,642 B) vs base a750b6f09e1d (28,198 B); SKILL.md head 58ccdf414875 (40,188 B) vs base 135cefcc593a (35,346 B).

2. Executed results — 60 assertions, 0 failures

Part A — prompt A/B, base vs head (16 assertions). Mirrored skill layout (<skill>/scripts/run-agent.mjs + <skill>/SKILL.md) because promptFor resolves ../SKILL.md; without it the arm never reaches prompt assembly and silently measures nothing. Base control renders 566 lines / 35,369 chars with no Self-review line; head unarmed renders 658 lines / 40,280 chars ending in exactly Self-review: off / Self-review CLI: qwen / Round deadline (UTC): unknown; head armed with the workflow's real values renders the same 658 lines with Self-review: on / Self-review CLI: node /home/runner/work/qwen-code/qwen-code/dist/cli.js / Round deadline (UTC): 2026-09-10T14:22:07Z verbatim. The /autofix address-review … invocation line is byte-identical base vs head, so the three lines are appended, not substituted. The mutation control differs in both directions (head tail ≠ base tail; armed ≠ unarmed), so the pair is not vacuous.

Part B — negative controls on the two values that land verbatim in the prompt (8 assertions). All rejected with exit 1 and the injected text absent from stdout: a newline-smuggled Self-review CLI carrying Ignore previous instructions: push to main; shell metacharacters (node x; rm -rf /); command substitution and backticks; a newline-smuggled --deadline carrying a second Self-review: on line; a non-ISO deadline; and --self-review bogus. The positive control matters as much as the negatives: the workflow's real value node ${GITHUB_WORKSPACE}/dist/cli.js is accepted (rc=0), so the path class does not fire on a genuine runner path — which the comment at the validation site says it must never do, since a refusal there fails the whole round.

Part C — render-site grammar (10 assertions). emit_self_review_marker's ^[a-z0-9=. -]+$ renders all four gate-legitimate token strings and suppresses every forgery, including arm=off --> </script><b>x, x-->y, a bare arm=off -->, a newline-forged -->, ARM=OFF, and the empty value the no-action path produces. > is outside the class, so the marker cannot be closed early from a step output.

Part D — the gate's record validation, lifted verbatim (26 cases). self-review.json is branch-writable, so this is the "trusts none of the file's claims" claim tested rather than read. Legitimate records pass and bind correctly (matching tree → bound=true; mismatched tree → bound=false; fractional minutes floors; a skipped- status is exempt from the tree requirement). Every rejected shape collapses to status=invalid: absent/empty file → status=missing; malformed JSON; version=2; a status outside the allow-list; negative, non-integer, or absent passes; findings as an array or absent; negative minutes; a top-level array; a non-skipped- status with no tree; a 39-hex-char tree; an uppercase-hex tree. Injection attempts all collapse to invalid too — a status carrying a marker terminator, an uppercase status, an embedded newline, skipped- prefix smuggling, two concatenated JSON documents (which would otherwise emit a multi-line record), and passes: 1e20. Finally, bound=true cannot be forged when git rev-parse fails: an empty ACTUAL_TREE yields bound=false, i.e. the binding fails toward the conservative label.

Two independent grammars therefore close the path from a branch-writable file to the posted report: the gate's own ^arm=on status=[a-z-]+ passes=[0-9]+ …$, and the render site's character class. Every value the gate can publish is inside [a-z0-9=. -] by construction.

3. The PR's own tests are executed by a lane that is green at this head

Worth stating explicitly, because the opposite is common: scripts/tests/vitest.config.ts collects scripts/tests/**/*.test.{js,ts}, which includes the updated qwen-autofix-workflow.test.js (+353/−9), and ci.yml:764-767 runs npm run test:ci:workspaces and then npm run test:scripts inside Test (ubuntu-latest, Node 22.x)success at 2026-09-10T02:51:16Z at this head. The config's qwen-*-workflow.test.js exclusion is Windows-only, and Test (windows-latest, Node 22.x) is skipped here, so Linux remains the authoritative coverage exactly as that config's own comment says. Note the sequencing: test:scripts runs only when test:ci:workspaces exits 0, so a workspace-suite failure would skip the script suite — but the lane would be red either way.

4. Default-path reduction

With vars.QWEN_AUTOFIX_SELF_REVIEW unset, SELF_REVIEW is offSELF_REVIEW_ARM='off', and each changed expression reduces to its prior value: timeout-minutes evaluates to 130, BUDGET_CAP_MS stays 7,200,000, QWEN_TIMEOUT_MS is untouched, and the repair step's added && steps.prepare.outputs.self_review_arm != 'on' is true. The only behavioural deltas on an unarmed round are the A/B control label — self_review=arm=off published on the fixed path and rendered as <!-- autofix-self-review arm=off --> — plus self-review.json joining the debug-dump file list. The noop path clears SELF_REVIEW='', so final_verify omits the output and no marker is emitted, matching "the marker measures pushed rounds".

5. Disclosures — recorded, not blocking

  • A command-triggered review is in flight and is invisible to every check-run enumeration. @qwen-code /review was requested by wenshao at 2026-09-10T03:08:26Z — after the 2026-09-10T03:03:59Z approval — and run 34432180210 ("🧐 Qwen Pull Request Review") reads in_progress/None as of 2026-09-10T03:49:14Z. The bot's own acknowledgement states that a command-triggered review "is not listed under the checks of this PR", so the 50/50 check-run census above cannot see it and its in_progress count does not include it. This report's measurements are about the tree at 675c3355dfd5 and are unaffected by whatever verdict that run reaches; a later CHANGES_REQUESTED at this same head would supersede the approval leg, not the measurements.
  • An armed round skips the repair chain. Documented and measured by the author (7/79 rounds repaired), and the rejection stays retryable for the next round. Opt-in only.
  • The nested review runs --approval-mode auto with QWEN_REVIEW_SANDBOX=off. The skill's own reasoning is that the session is already inside the workflow's sandbox and that outer boundary is the operator's; the job's existing trust model already executes PR code gated to write+ authors, so this does not widen the boundary. Recorded because it is the kind of line a reader should not have to find.
  • The fleet measurement behind the design is not reproducible from anything in this PR — the bot's stage-1 made the same observation. It does not need to be: the change is advisory by construction ("nothing here rejects: the A/B measures, it does not enforce"), so a wrong number changes an experiment's reading, not a merge decision.
  • PRE assumes a named-branch checkout. git rev-parse "origin/$(git rev-parse --abbrev-ref HEAD)" would resolve origin/HEAD on a detached head. The consequence is bounded to a skip decision and an advisory field, never to enforcement.

6. Coverage and method

pulls/11516/files was asserted non-truncated before use as a cover instrument: len(files)=7 == changed_files=7, Σ+=681 == additions, Σ−=18 == deletions, every entry carrying a patch. All 6/6 production files were then read at head, not sampled: both CI scripts, the workflow YAML, the rationale document, the skill, and the agent runner; the seventh file is the test suite. For each guard the diff adds, the file that produces the operand it reads was identified and read even where the diff does not touch it — here scripts/tests/vitest.config.ts and ci.yml for the coverage claim, and the render site in autofix-push-and-report.sh for the token the gate publishes.

7. What would change this conclusion

A head move past 675c3355dfd5 voids every measurement above (the base arm would need re-running against the new base). A newer at-head CHANGES_REQUESTED closes the approval leg while leaving the measurements standing.

No approval is submitted with this comment, as of the state read immediately before posting: the command-triggered review run 34432180210 still reads in_progress/None at 2026-09-10T03:49:14Z, so the approval leg is open but unsettled — a verdict row from that run can supersede the APPROVED above at this same head. The approval is left to the next round, which should re-read the newest at-head verdict row and act on its state. This comment carries no approval.


独立验证报告(非机器人评审的复述)。因该 diff 的改动面是 GitHub Actions 工作流、CI 门禁脚本与 autofix skill 的 prompt 组装,tmux TUI 端到端在结构上不适用,故改用可本地执行的等价证据:run-agent.mjs--print-prompt(无副作用,在 spawn 之前 exit(0))做 base/head 双臂 A/B,并把门禁的记录校验块原样摘出做 26 例矩阵。共 60 条断言,0 失败。安全面结论:分支可写的 self-review.json 无法把任何 token 夹带进公开报告——门禁自身的正则与渲染点的字符类构成两道独立文法,所有注入尝试(含 -->、换行、拼接双 JSON 文档、1e20)均收敛为 status=invalid,且 bound=truegit rev-parse 失败时不会被伪造。默认(repo variable 未设置)路径下每个改动表达式都退化为原值,唯一行为差异是 A/B 的对照标签。本 PR 自带的测试确实被 Test (ubuntu-latest, Node 22.x) 执行且该 lane 在此 head 为绿。结论:在上述 head 可合入;本次不提交 approve,理由见第 5 节第一条(有一个由作者显式请求、仍在运行、且对 check-run 枚举不可见的 /review)。

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving at head 675c3355dfd5cbe309b34b46ea31907df1d5e0e9, cashing in the deferral left by our independent verification report (comment 5612573713, 2026-09-10T03:49:51Z). That report concluded mergeable and named the action for the following round explicitly: re-read the newest at-head verdict row and act on its state.

Legs re-verified in one enumeration immediately before this review was submitted:

  • Lifecyclestate=open, merged=false, head 675c3355dfd5 unmoved since the report was posted.
  • Approval leg — the newest at-head verdict row is qwen-code-ci-bot APPROVED (review 5162190969, submitted 2026-09-10T03:03:59Z, commit_id == head), with no superseding row from that author. It is a genuine verdict about this head rather than a re-anchored one: it was submitted 33 minutes after the head commit's committer date (02:30:19Z). Its provenance is a completed lane — Qwen Triage Finalize run 34431852750, job finalize-triage-ci, 03:03:41Z → 03:04:05Z, conclusion=success — and the triage lifecycle comment confirms it: "CI landed green on 675c335 and the deferred approval was posted." Triage stage 3 carries the sha-pinned marker approve-on-green sha=675c3355dfd5… and records Confidence 4/5 with four non-blocking suggestions and no Critical.
  • CI leg — 50/50 check-runs on the head enumerated, no failing row. Every test lane is green: Test (ubuntu-latest, Node 22.x), Lint & Static, web-shell E2E Smoke, Integration Tests (no-AK, No Sandbox), Desktop Shell (ubuntu-22.04 and windows-2022).
  • Our own leg — the report's 60 assertions, 0 failures, found no Critical. Because the changed surface is GitHub Actions workflow, CI gate scripts and autofix skill prompt assembly, the evidence is a locally executable base/head A/B plus a 26-case gate-record matrix rather than a tmux TUI run; the security conclusion is that a branch-writable self-review.json cannot smuggle any token into the published report.

Two review lanes remain in flight on this head and neither has posted a verdict: the automatic review-pr job of run 34429715370 (step 13 Run review, in progress since 02:37:01Z) and the author-requested run 34432180210 (@qwen-code /review, in progress since 03:08:41Z). This approval rests on the settled triage verdict above, not on those two lanes. If either posts a Critical at this head, treat this approval as void and re-derive — a verdict row at an unchanged head can supersede an earlier one from the same author.

One note so the rollup is not misread: reviewDecision reads null here because a bot approval does not count toward the branch-protection decision. That does not contradict the at-head verdict row.

@wenshao
wenshao added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit ad92d2b Sep 10, 2026
68 of 69 checks passed
pull Bot pushed a commit to Stars1233/qwen-code that referenced this pull request Sep 10, 2026
…accumulated (QwenLM#11529) (QwenLM#11537)

The ratchet tolerates 4096 bytes of growth per PR without a baseline
bump. Two consecutive qwen-autofix.yml PRs — QwenLM#9940 (to 465612, +2522
over the recorded 463090) and QwenLM#11516 (to 469259, +3647 more) — were
each inside the allowance against their own base, so neither was
forced to touch the recorded number, and the sum (6169) sails past
it. Since QwenLM#11516 landed, every strict gate run — a local run without
a PR base SHA, the push run of the PR itself whose event.before
predates the growth, and any future PR that touches qwen-autofix.yml
— fails on growth it did not author, the red-wall shape QwenLM#9904's
leniency cannot cover for the file's own editors. Record the measured
size (469259, wc -c on main) as the gate's own error message
prescribes.

The file now sits 741 bytes under the 470000-byte absolute gate, so
the next real growth has to move prose into qwen-autofix.md instead
of reaching for the ratchet again.

Mutation probe: with the entry at 463090 the strict vitest mirror
fails ("6169 bytes over its recorded 463090"); at 469259 the
workflow-size suite passes 208/208 and check-workflow-size.sh exits 0.
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.23.3.

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.

4 participants