Skip to content

fix(ci): make autofix busy detection fail closed and mark dispatched PRs - #9329

Merged
wenshao merged 3 commits into
QwenLM:mainfrom
wenshao:fix/autofix-busy-detection-fail-closed
Aug 18, 2026
Merged

fix(ci): make autofix busy detection fail closed and mark dispatched PRs#9329
wenshao merged 3 commits into
QwenLM:mainfrom
wenshao:fix/autofix-busy-detection-fail-closed

Conversation

@wenshao

@wenshao wenshao commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Hardens the autofix busy-PR detection in the fleet scan, in two parts:

  1. Fail-closed enumeration. The busy-PR skip reads live runs and their job lists via the API. Previously every failure there was swallowed silently (fail-open), and the scan dispatched as if nothing was busy. Now any enumeration failure (the run list, or one run's jobs view) empties this scan's candidate set with a warning; the next tick retries with fresh reads. A forced dispatch keeps its explicit-override semantics and is not emptied.

  2. Dispatch-pending marker. Between dispatch and leg materialization there is a visibility window: build-cli runs in between, and the matrix leg does not exist in the live-run jobs view until it expands. The scan now stamps a pending commit-status marker (qwen-autofix/dispatch-pending) on the PR head when it dispatches a target, and treats a marker fresher than 30 minutes as busy. The address leg re-stamps the marker success on checkout; a run that dies before materializing leaves a marker that expires by age. The check piggybacks on the statusCheckRollup already fetched per candidate — zero extra API calls. Same-repo heads only: a fork head sha cannot carry a status in this repo, so fork duplicates stay covered by the address-time revalidation.

Why it's needed

Measured on 2026-08-16 (full evidence in #9296): the busy enumeration missed address legs that had been running or queued for 3–12 minutes, and overlapping scans re-dispatched the same PRs. Each duplicate burned one build-cli (~5 min) and then cancelled a queued sibling leg through the per-PR group's latest-wins queue; the cancelled parent runs were a major contributor to the workflow's 59% cancellation rate. A duplicate costs far more than one skipped scan pass, so the enumeration now fails closed; the marker closes the remaining window where no leg exists to see yet.

Design note: the marker was originally planned as a queued check-run, but the check-run creation API requires GitHub App authentication (PAT gets HTTP 403), and this workflow authenticates with a PAT. Commit statuses work with the existing credential and surface the same fields (context, state, startedAt) in the PR's statusCheckRollup, which made them the natural substrate.

Reviewer Test Plan

How to verify

This is a CI-workflow change; behavior is observable in scan logs and run statistics:

  1. Fail-closed: when live-run enumeration is healthy, scans dispatch exactly as before (the busy skip is unchanged). There is no easy way to force an API failure on demand — verify the code path reads as: any gh run list / gh run view failure sets the flag, and the flag empties the candidate set unless a forced PR is set.
  2. Marker: dispatch a target on a same-repo PR and start a second scan within ~30 minutes while build-cli is still running. Expected: the second scan logs ⏳ #<PR>: dispatch pending … — skipping and a busy fleet row for it, instead of re-dispatching. After the leg checks out, the marker flips to success and subsequent scans inspect the PR normally.
  3. Orphan marker: a run killed between dispatch and leg materialization leaves a pending status; scans skip that PR for at most 30 minutes, then TTL expiry restores normal dispatch (no permanent busy).
  4. Fork PRs: no status is stamped (the head sha lives in the fork); fork candidates behave exactly as before.

Local verification performed: the full workflow parses as YAML and passes yamllint 1.35.1 (the CI-pinned version) with zero findings; both changed run blocks pass bash -n and shellcheck at error severity; the marker's jq filter was tested against real statusCheckRollup shapes captured from a live probe (fresh pending → busy, stale pending → free, success → free, missing rollup → free); the fail-closed block was driven through a stubbed-gh harness covering healthy enumeration, run-list failure (fleet emptied), run-list failure with forced PR (candidates kept), and jobs-view failure (fleet emptied). The status-based marker round-trip (create → rollup shape → re-stamp) was probed live against a scratch fork.

Evidence (Before & After)

Before: 2026-08-16 — scans at 11:27Z and 11:37Z re-dispatched #9255/#9027 while address legs for those PRs already existed (running 12 minutes / queued 3 minutes); the duplicate legs queued behind the originals and were cancelled by the latest-wins queue after wasting their build-cli runs. After: such overlaps are skipped at scan time via the jobs enumeration (fail-closed when unreadable) plus the dispatch-pending marker (covers the pre-materialization window).

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

N/A — workflow change only; the workflow runs on ubuntu-latest.

Risk & Scope

  • Main risk or tradeoff: a transient API hiccup now skips one scan pass entirely instead of dispatching on partial information — bounded to one cron tick of delay, and much cheaper than duplicate address legs. The marker can also keep a scan away for up to 30 minutes after a run dies pre-materialization; the leg releases the marker as soon as it checks out, so the common case clears within minutes.
  • Not validated / out of scope: the remaining Qwen Autofix: review-event storms and duplicate address dispatch waste runner capacity #9296 items (batching finding replies, cron-group semantics, resolve-pr diagnostics) are untouched; this PR is the busy-detection hardening only.
  • Breaking changes / migration notes: none. The new status context appears on dispatched PR heads as an informational check entry.

Linked Issues

Part of #9296 (first P1 item). No closing keyword — the issue still tracks the replies-batching and P2 items.

中文说明

这个 PR 做了什么

加固 fleet scan 的 busy-PR 检测,分两部分:

  1. fail-closed 枚举:busy 跳过逻辑通过 API 读取存活 run 及其 job 列表,此前所有失败被静默吞掉(fail-open),scan 会当作没有 busy 照常派发。现在任何枚举失败(run 列表、或某个 run 的 jobs 视图)都会清空本轮 scan 的候选集并告警,下一个 tick 用新鲜读取重试。forced 派发保留显式覆盖语义,不被清空。

  2. dispatch-pending 标记:派发与 leg 物化之间存在可见性窗口——中间隔着 build-cli,matrix leg 在展开前不存在于 live-run jobs 视图。scan 派发 target 时在 PR head 打一个 pending 的 commit status 标记(qwen-autofix/dispatch-pending),30 分钟内的新鲜标记视为 busy。address leg 在 checkout 后把标记重打为 success;run 在物化前死掉留下的标记按年龄过期。检查直接复用每个候选已经获取的 statusCheckRollup——零额外 API 调用。仅限同仓库 head:fork 的 head sha 无法在本仓库承载 status,fork 重复派发继续由 address-time revalidation 兜底。

为什么需要

2026-08-16 实测(完整证据见 #9296):busy 枚举漏检了已运行/排队 3–12 分钟的 address leg,并发 scan 重复派发同一 PR。每个重复 leg 浪费一次 build-cli(约 5 分钟),然后通过 per-PR 组的 latest-wins 队列取消先排队的兄弟 leg;被取消的父 run 是工作流 59% 取消率的主要贡献者。重复派发的代价远高于跳过一轮 scan,因此枚举改为 fail-closed;标记则补上"leg 尚不存在"的剩余窗口。

设计说明:标记原计划用 queued 状态的 check-run,但 check-run 创建 API 需要 GitHub App 认证(PAT 返回 HTTP 403),而本工作流用 PAT 认证。commit status 用现有凭据即可创建,且在 PR 的 statusCheckRollup 中暴露相同字段(context、state、startedAt),是天然的载体。

Reviewer 测试计划

如何验证

这是 CI 工作流变更,行为体现在 scan 日志与 run 统计:

  1. fail-closed:API 健康时 scan 派发行为与之前完全一致(busy 跳过逻辑未变)。API 失败无法按需构造——请核对代码路径:任何 gh run list / gh run view 失败置位标志,标志清空候选集,除非存在 forced PR。
  2. 标记:对同仓库 PR 派发一个 target,在 build-cli 仍在运行的 ~30 分钟窗口内启动第二次 scan。预期:第二次 scan 输出 ⏳ #<PR>: dispatch pending … — skipping 和 busy fleet 行,而不是重复派发。leg checkout 后标记翻转为 success,后续 scan 正常检查该 PR。
  3. 孤儿标记:run 在派发与物化之间被杀会留下 pending status;scan 最多跳过该 PR 30 分钟,TTL 过期后恢复正常派发(不会永久 busy)。
  4. fork PR:不打标记(head sha 在 fork 里);fork 候选行为与之前完全一致。

已完成的本地验证:完整工作流 YAML 解析通过,yamllint 1.35.1(CI 固定版本)零问题;两个改动的 run 块通过 bash -n 和 error 级 shellcheck;标记 jq 过滤器用 live 探测捕获的真实 statusCheckRollup 形态测试(新鲜 pending → busy、过期 pending → 放行、success → 放行、rollup 缺失 → 放行);fail-closed 块用 stub gh 的 harness 覆盖了健康枚举、run 列表失败(fleet 清空)、run 列表失败但有 forced PR(候选保留)、jobs 视图失败(fleet 清空);status 标记的创建→rollup 形态→重打全流程在临时 fork 上 live 探测过。

证据(改动前后)

改动前:2026-08-16——11:27Z 与 11:37Z 的 scan 在 #9255/#9027 的 address leg 已存在(运行 12 分钟/排队 3 分钟)时重复派发;重复 leg 排在原 leg 后面,浪费 build-cli 后被 latest-wins 队列取消。改动后:这类重叠在 scan 时经 jobs 枚举(不可读时 fail-closed)加 dispatch-pending 标记(覆盖物化前窗口)被跳过。

测试环境

OS 状态
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

环境(可选)

N/A——仅工作流变更;工作流运行在 ubuntu-latest。

风险与范围

  • 主要风险或权衡:一次瞬时 API 故障现在会让整轮 scan 跳过派发,而不是基于部分信息派发——延迟上限为一个 cron tick,远比重复 address leg 便宜。run 在物化前死掉时,标记最多让 scan 避让 30 分钟;leg 一旦 checkout 就释放标记,常见情况几分钟内解除。
  • 未验证 / 超出范围:Qwen Autofix: review-event storms and duplicate address dispatch waste runner capacity #9296 的其余项(finding 回复批量化、cron 组语义、resolve-pr 诊断)不在本 PR;本 PR 只做 busy 检测加固。
  • 破坏性变更 / 迁移说明:无。新的 status context 会以信息性检查项出现在被派发 PR 的 head 上。

关联 Issue

属于 #9296(第一个 P1 项)。不使用关闭关键字——该 issue 还跟踪回复批量化与 P2 项。

Reviews on merged/closed PRs have nothing to address, yet each one started an autofix run that spun up a runner only to exit no-op. Observed 2026-08-16: 24+ finding-reply reviews on merged QwenLM#9222 and 26 runs on merged QwenLM#9189 within minutes (issue QwenLM#9296). Add a PR open-state clause to the route prefilter; the scheduled scan remains the backstop, and address-time revalidation already drops targets whose PR closed after dispatch.
Silent API failures in the busy-PR enumeration re-dispatched PRs whose address legs were already running or queued (issue QwenLM#9296): each duplicate burned one build-cli (~5 min) before cancelling a queued sibling leg through the per-PR group's latest-wins queue.

- Any enumeration failure (run list or per-run jobs view) now empties the scan's candidate set for this pass; a forced dispatch keeps its explicit-override semantics.
- Stamp a pending commit-status marker (qwen-autofix/dispatch-pending) on the PR head at dispatch and treat it as busy while fresher than 30 minutes; the address leg re-stamps it success on checkout. This covers the scan->build-cli window where the matrix leg does not exist in the live-run jobs view yet. Commit statuses only: the check-run creation API needs a GitHub App, and the workflow authenticates with a PAT.

Refs QwenLM#9296
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Re-run after the autofix loop pushed 9dbaff58 — gate re-checked against the new head.

Template looks good ✓

Problem: observed, not theoretical. Dated evidence from 2026-08-16 (scans at 11:27Z/11:37Z re-dispatched #9255/#9027 while their address legs had been running/queued for 3–12 minutes), quantified cost (one build-cli burned per duplicate, cancelled sibling legs via the latest-wins queue feeding the 59% cancellation rate), tracked as the first P1 item of #9296. The round-2 commit does not change the problem statement.

Direction: aligned. This is the repo's own autofix fleet burning CI minutes on duplicate dispatches, and the change stays scoped to the workflow's busy-detection path. The design note on commit statuses vs check-runs (PAT gets HTTP 403 on check-run creation) shows the alternatives were actually evaluated. Internal CI tooling — no external CHANGELOG reference applies; the area is squarely relevant.

Size: not applicable — no core-module paths touched. Production logic is the workflow file (+165/−27 including round 2); the other file (+434/−2) is the workflow's contract-test suite and is excluded from size accounting. Well under every advisory threshold.

Approach: scope still right, and round 2 is strictly review-driven — it adds exactly the fixes the first passes asked for (the HAS_PENDING_CHECKS exemption, same-repo + dry-run guards on all three status writers, the discard-path release, the narrowed fail-closed carve-out, the enum_failed issue-phase signal, error tails) plus behavioral replay tests, and nothing else. Two hygiene notes, neither blocking: the body still says "two parts" while the diff carries a third change (the pull_request_review route gate — well-commented in the workflow itself), and the shepherd busy-set duplication was deferred to a follow-up because fixing it touches files outside this PR's footprint (thread kept open).

Risk: no elevated risk signals — no high-risk paths matched. The fail-closed tradeoff (one skipped cron tick per transient API hiccup) remains bounded and cheaper than a duplicate leg.

Moving on to code review. 🔍

中文说明

autofix 循环推送 9dbaff58 后的重跑——针对新 head 重新过 gate。

模板完整 ✓

问题:已观测到,非理论性问题。带日期的证据(2026-08-16,11:27Z/11:37Z 的 scan 在 #9255/#9027 的 address leg 已运行/排队 3–12 分钟时重复派发),代价已量化(每个重复 leg 浪费一次 build-cli,latest-wins 队列取消兄弟 leg 推高 59% 取消率),属于 #9296 的第一个 P1 项。第二轮提交未改变问题陈述。

方向:对齐。这是仓库自身 autofix fleet 在重复派发上浪费 CI 时间,改动范围限于工作流的 busy 检测路径。commit status 与 check-run 的设计说明(PAT 创建 check-run 返回 403)表明确实评估过备选方案。内部 CI 工具,不适用外部 CHANGELOG 参照;该领域完全相关。

规模:不适用——未触及核心模块路径。生产逻辑为工作流文件(+165/−27,含第二轮);另一文件(+434/−2)是工作流契约测试套件,不计入规模。远低于所有提示阈值。

方案:范围依然合理,且第二轮完全由评审驱动——恰好补齐了前几轮要求的修复(HAS_PENDING_CHECKS 豁免、三处 status 写入的同仓库 + dry-run 守卫、丢弃路径释放、收窄的 fail-closed 豁免、enum_failed issue 阶段信号、错误尾部)外加行为回放测试,没有其他内容。两条卫生问题,均不阻塞:body 仍写"两部分"而 diff 实际含第三处改动(pull_request_review 路由门——工作流内注释充分);shepherd 的 busy 集重复被推迟到后续跟进,因为修复需要触碰本 PR 足迹之外的文件(线程保持开放)。

风险:无升级风险信号——未命中高风险路径。fail-closed 的代价(一次瞬时 API 故障跳过一轮 cron tick)有上限,且比重复 leg 便宜。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 9dbaff587269439c6d5ffcf7409af3d01033fe6b · re-run with @qwen-code /triage

@wenshao

wenshao commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 17, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Code review

This is the second pass; the first reviewed fcb6941 and flagged the stranded-marker problem, which round 2 fixes. My independent proposal for that fix was: exempt the marker's own context from the generic pending-checks gate so its dedicated TTL is the only clock on it, guard every writer same-repo + dry-run, release on the discard path that exits before checkout, and pin all of it with tests that fail pre-fix. Round 2 matches that shape on every point, so this pass verified the fix sites in the head itself and chased the edges they introduce.

Verified at 9dbaff58 (read from the workflow at head, not just the diff):

  • The stranded-marker Critical is closed: HAS_PENDING_CHECKS now binds --arg ctx "${DISPATCH_STATUS_CONTEXT}" and exempts exactly (.context // "") == $ctx. A stranded PENDING marker alone no longer blocks; genuine in-flight checks still block alone and alongside the marker; the 330-minute aging still applies to real checks; foreign status contexts still block. The new replay test asserts all six classes and fails against the pre-round branch.
  • The fail-closed carve-out is bounded to explicit dispatches: it takes both FORCED_PR and EVENT_NAME == workflow_dispatch, so trusted pull_request_review scans (which also carry FORCED_PR) stay fail-closed, matching the cap-refused gate's existing split. Both arms are replayed in the test.
  • An enumeration failure can no longer read as "no PR needs work": the scan emits enum_failed, and both the issue phase's if: and its concurrency-group predicate require enum_failed != 'true' inside their schedule-only clauses. The equivalence pin between the two expressions is updated on both sides.
  • All three status writes are guarded same-repo + DRY_RUN (the prepare step gained the missing DRY_RUN env), and the INELIGIBLE discard — which exits before the checkout-path release — now releases the marker on the live head.
  • Enumeration failures keep the last 200 bytes of stderr in the ::warning:: and the fleet row, so transient API noise and PAT decay are distinguishable on-call.

Remaining items — all non-blocking, all logged for human follow-up (the autofix loop is in critical-only mode and correctly will not touch them):

  1. A stranded PENDING status is never cleared on the PR page when the leg never materializes (build-cli failure, cancellation, pre-checkout crash): commit statuses don't expire, so the entry lingers visually. Dispatch correctness is unaffected — the reader TTL and the gate exemption both age it out — but a failure-keyed finalizer re-stamping the dispatched heads would close it (review thread R2-1).
  2. The discard-path release cannot fire for the one discard reason whose guard inputs are both unknowable — a failed live-metadata fetch (LIVE_XREPO defaults true, LIVE_HEAD_OID empty). That is fail-closed by design; the cost is a bounded skip that self-heals at the 30-minute TTL (R2-2).
  3. The lifecycle test pins cross-site identity but only the shape at the writer sites — a mutation analysis in the review thread shows a pendingsuccess flip, a wrong sha variable, or a dropped continue would each survive the suite. The code as written is correct; this is test hardening (R2-3).
  4. An explicit workflow_dispatch onto a PR with a fresh stranded marker waits out the TTL — consistent with the existing busy-skip, which also applies to forced PRs, and self-healing.
  5. Hygiene: the body still says "two parts" for a three-change diff; the route gate is documented in the workflow itself.

Marker lifecycle with the TTL-exemption path:

sequenceDiagram
    participant P1 as Fleet scan
    participant P2 as Commit status on PR head
    participant P3 as build-cli
    participant P4 as Address leg
    P1->>P2: stamp PENDING at dispatch
    Note over P1,P4: visibility window - leg not yet in jobs view
    P1->>P3: emit target, build CLI bundle
    P3->>P4: matrix leg materializes
    P4->>P2: re-stamp SUCCESS at checkout or discard
    Note over P2: after the 30m TTL the marker blocks nothing anywhere
Loading

Testing

Unattended CI run — no PR code was built or executed here; the evidence below is the PR's own CI results fetched via the API. tmux: N/A — workflow-only change with no product surface. The yamllint/shellcheck/stubbed-gh harness results in the description are the author's local verification, attributed as such; the CI evidence for the contract suite is the green Test (ubuntu-latest, Node 22.x) check below, which runs scripts/tests/qwen-autofix-workflow.test.js including the three new behavioral replay tests.

CI at the reviewed commit is fully settled and all green — both pull_request-event workflow runs (Qwen Code CI, Security Checks) completed with success; zero failures, zero pending. The macOS/Windows test legs are skipped per the fork-PR matrix restriction.

Sandboxed lanes: a /verify run launched from this triage is in flight and will post its own report, but it A/B-builds the CLI bundle, which this diff doesn't touch — at most it confirms payload equivalence. The scan loop itself is only observable in the fleet table after merge (fail-closed and busy: dispatch-pending marker live rows are the purpose-built signals), so I'm naming that as the substantiation path rather than pointing at a lane that cannot run this code.

Check Conclusion
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
route ✅ success
Secret scan (TruffleHog) ✅ 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,失败项排在最前。

中文说明

代码审查:这是第二次审查;第一次审查 fcb6941 时指出了滞留标记问题,第二轮已修复。我对该修复的独立方案是:让通用 pending 检查门豁免标记自身的 context,使其专用 TTL 成为唯一时钟;所有写入点加同仓库 + dry-run 守卫;在 checkout 之前退出的丢弃路径上释放标记;并用对修复前分支失败的测试钉住全部行为。第二轮逐点吻合,因此本次核对 head 中的修复位置并追查其引入的边缘情况。

已在 9dbaff58 验证:滞留标记 Critical 已关闭——HAS_PENDING_CHECKS 绑定 --arg ctx 并恰好豁免该 context,滞留标记单独不再阻塞、真实在跑检查仍阻塞(单独及与标记并存)、330 分钟老化仍作用于真实检查、外部 context 仍阻塞,新回放测试断言全部六类且对修复前分支失败。fail-closed 豁免收敛为显式派发(需同时满足 FORCED_PR 与 workflow_dispatch),受信 pull_request_review scan 保持 fail-closed,与既有 cap 拒绝门一致。枚举失败不再被误读为"无 PR 需要处理":scan 输出 enum_failed,issue 阶段 if: 与并发组谓词的 schedule 子句均要求其非 true。三处 status 写入均有同仓库 + DRY_RUN 守卫(prepare 步补上缺失的 DRY_RUN env),checkout 前退出的 INELIGIBLE 丢弃现在会在 live head 上释放标记。枚举失败保留 stderr 末 200 字节进告警与 fleet 行,便于区分瞬时抖动与 PAT 失效。

遗留项(均不阻塞,均已记录待人工跟进):① leg 未物化时滞留的 PENDING status 不会从 PR 页面清除(状态不过期,仅视觉残留;派发正确性不受影响,TTL 与门豁免都会使其老化)。② 元数据获取失败的丢弃无法触发释放(两个守卫输入都不可知,设计上 fail-closed;代价是 30 分钟 TTL 自愈的有界跳过)。③ 生命周期测试钉住跨点一致性但写入点仅钉形状——突变分析表明 state 翻转/错误 sha 变量/丢失 continue 可通过套件;现代码正确,属测试加固。④ 显式 workflow_dispatch 撞上新鲜滞留标记需等 TTL——与既有 busy 跳过行为一致且自愈。⑤ 卫生:body 仍写"两部分"而 diff 含三处改动,路由门已在工作流内注释。

测试:无人值守 CI 运行,未构建或执行 PR 代码;证据为经 API 获取的 PR 自身 CI 结果。tmux 不适用(纯工作流改动,无产品界面)。描述中的本地验证结果仅作转述;契约套件的 CI 证据是下方绿色的 ubuntu Test 检查(含三个新行为回放测试)。被审 commit 的 CI 已全部完成且全绿——两个 pull_request 事件工作流运行均成功,零失败零待运行;macOS/Windows 测试腿按 fork PR 矩阵限制跳过。沙箱通道:本次 triage 触发的 /verify 正在运行并将自行发布报告,但其对比的 CLI 构建产物与本 diff 无关;scan 循环只能合并后经 fleet 表观测,故直接说明实证路径而不指向无法运行此代码的通道。

Qwen Code · qwen3.8-max

Reviewed at 9dbaff587269439c6d5ffcf7409af3d01033fe6b · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — every finding from the first pass is resolved in round 2, including the stranded-marker Critical, with regression tests that fail against the pre-fix branch; what remains is known-bounded, self-healing, and logged for human follow-up.

Stepping back: this PR is in better shape than the head the first pass reviewed. The one real reservation back then — the orphan window actually running on the 330-minute stale-check clock instead of the promised 30 minutes — is fixed with an exemption that is exactly the marker's own context, and the fix was reproduced before it was written (the replay returns the blocking value on the pre-round branch where the test asserts the released one). Every edge I chased at the new head degrades the same way: a stranded status lingers visually on the PR page but can't block dispatch; a failed metadata fetch strands the marker but the TTL releases it; an explicit dispatch into a fresh marker waits at most one TTL, same as the existing busy skip. The failure budget everywhere is "skip one scan tick", never "duplicate dispatch".

The remaining nits — the body still saying "two parts", the stamp-site test pins that would survive a state-flip mutation, the follow-up on the shepherd duplication — are recorded above and in the review thread; none is worth another round trip on a production CI workflow where the human review already landed (a maintainer approval stands on this head). The fork-refactor guardrail does not apply (fix, not refactor), and the author is a maintainer. CI is fully settled and green on the reviewed commit, so no deferral. Approving pinned to 9dbaff58 — this supersedes this account's stale changes-requested state on the earlier head, whose sole Critical is what round 2 fixed.

中文说明

评分 4/5:第一轮的所有发现均已在第二轮解决,包括滞留标记 Critical,且带有对修复前分支失败的回归测试;遗留项均为已知有界、可自愈、已记录待人工跟进。

退一步看:这个 PR 的状态好于第一轮审查的 head。当时唯一的实质性保留意见——孤儿窗口实际运行在 330 分钟 stale 时钟而非承诺的 30 分钟——已通过恰好限定为标记自身 context 的豁免修复,且修复先复现后落码(回放对修复前分支返回阻塞值而测试断言放行值)。新 head 上追查的每个边缘都以同样方式降级:滞留 status 仅在 PR 页面视觉残留、不阻塞派发;元数据获取失败使标记滞留但 TTL 会放行;显式派发撞上新鲜标记最多加等一个 TTL,与既有 busy 跳过一致。所有失败预算都是"跳过一轮 scan tick",而非"重复派发"。

遗留的小问题——body 仍写"两部分"、打标记点测试可被 state 翻转突变通过、shepherd 重复的后续跟进——已记录在上文与评审线程中;对于一个已有人工评审落地的生产 CI 工作流,这些不值得再走一轮。fork-refactor 护栏不适用(fix 而非 refactor),作者为维护者。被审 commit 的 CI 已全部完成且全绿,无需推迟。批准固定于 9dbaff58——此批准取代本账号在更早 head 上过期的 changes-requested 状态,该评审唯一的 Critical 正是第二轮修复的内容。

Qwen Code · qwen3.8-max

Reviewed at 9dbaff587269439c6d5ffcf7409af3d01033fe6b · 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. ✅

@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.

Partially reviewed — gaps disclosed.

Not reviewed: reverse audit — stopped before round 6 by the review time budget.

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

中文说明

仅完成部分审查,审查缺口已披露。

未审查:反向审计——评审时间预算不足,未能开始第 6 轮。

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

— qwen3.8-max via Qwen Code /review (v0.21.13)

# the leg does not exist in the live-run jobs view, so an overlapping scan
# would re-dispatch the same PR. The scan treats a PENDING marker fresher
# than DISPATCH_STATUS_TTL_MINUTES as busy (a run that dies before the leg
# materializes leaves a marker that expires by age). Commit statuses only —

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.

[Critical] A stranded dispatch-pending PENDING marker is additionally caught by the pre-existing HAS_PENDING_CHECKS gate (~line 3067), which ages pending entries out with PENDING_STALE_MIN=330 — so the marker blocks the PR for up to ~330 minutes, not the 30-minute TTL documented here ("expires by age"). StatusContext entries carry no .workflowName (they pass the != "Qwen Autofix" carve-out) and no .name (not in NON_BLOCKING_CHECKS=["review-pr"]), and their startedAt stays inside the 330-minute PENDING_CUTOFF. The diff never exempts its own status context from that gate — Failure scenario: a scan stamps PENDING, the run dies before the leg materializes (build-cli failure/cancellation); after 30 min the new TTL check correctly passes, but HAS_PENDING_CHECKS still matches and every scan skips the PR as "active checks in flight" for up to ~330 min (self-heals only at 330 min or a moved head). The block covers scheduled AND forced scans (no FORCED_PR exemption at that gate), and /retry cannot clear a commit status, so there is no in-band lever.

Witness (live probe on this PR's head):

with a PENDING probe status present: the workflow's exact HAS_PENDING_CHECKS filter
returned the entry -> HAS_PENDING_CHECKS=true
age-threshold arm (captured gh StatusContext shape):
marker age=29m  | new-30m-marker-check-fires=yes | HAS_PENDING_CHECKS=true
marker age=60m  | new-30m-marker-check-fires=no  | HAS_PENDING_CHECKS=true
marker age=331m | new-30m-marker-check-fires=no  | HAS_PENDING_CHECKS=false

Suggested fix — exempt the marker's context from HAS_PENDING_CHECKS (NON_BLOCKING_CHECKS cannot express this: it matches .name, which StatusContext entries lack):

# in the HAS_PENDING_CHECKS jq: add --arg ctx "${DISPATCH_STATUS_CONTEXT}"
| select((.context // "") != $ctx)
中文说明

【严重】 滞留的 dispatch-pending PENDING 标记还会被既有的 HAS_PENDING_CHECKS 门(约 3067 行)命中:该门用 PENDING_STALE_MIN=330 作为过期阈值——因此标记会把 PR 阻塞最长约 330 分钟,而不是此处文档承诺的 30 分钟("expires by age")。StatusContext 条目没有 .workflowName(能通过 != "Qwen Autofix" 豁免),也没有 .name(不在 NON_BLOCKING_CHECKS=["review-pr"] 中),且其 startedAt 在 330 分钟的 PENDING_CUTOFF 之内。diff 没有把自身的 status context 从该门中排除——失败场景:scan 打上 PENDING 后 run 在 leg 物化前死亡(build-cli 失败/被取消);30 分钟后新的 TTL 检查正确放行,但 HAS_PENDING_CHECKS 仍然命中,之后每轮 scan 都会以 "active checks in flight" 跳过该 PR,最长约 330 分钟(只在 330 分钟到期或 head 移动时自愈)。该阻塞对定时 scan 和 forced scan 都生效(该门没有 FORCED_PR 豁免),且 /retry 无法清除 commit status,因此没有任何带内解法。

证据(在本 PR head 上的 live 探针):存在 PENDING probe status 时,工作流原样的 HAS_PENDING_CHECKS 过滤器返回该条目(HAS_PENDING_CHECKS=true);年龄阈值实验(用捕获的真实 gh StatusContext 形态):标记 29 分钟 → 新 30 分钟检查命中且 HAS_PENDING_CHECKS=true;60 分钟 → 30 分钟检查放行但 HAS_PENDING_CHECKS=true;331 分钟 → HAS_PENDING_CHECKS=false。

建议修复:把标记的 context 从 HAS_PENDING_CHECKS 中排除(NON_BLOCKING_CHECKS 表达不了:它匹配的是 StatusContext 没有的 .name)——在该门的 jq 中加 --arg ctx "${DISPATCH_STATUS_CONTEXT}"| select((.context // "") != $ctx)

— qwen3.8-max via Qwen Code /review (v0.21.13)

Comment thread .github/workflows/qwen-autofix.yml Outdated
Comment on lines +2831 to +2832
if [[ -z "${FORCED_PR}" ]]; then
CANDIDATES=''

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.

[Suggestion] The fail-closed exemption is broader than its stated justification: FORCED_PR is set for every trusted pull_request_review scan too (route emits pr_number for them; the job-level comment ~line 3330 documents FORCED_PR "is ALSO set for every trusted pull_request_review … which is not an explicit dispatch"), so event-driven review scans stay fail-open on enumeration failure, while the added FAIL-CLOSED comment says any enumeration failure empties THIS scan's candidate set — Failure scenario: an enumeration failure (rate limit / 5xx — the condition that produced the 2026-08-16 duplicates) during a trusted-review-event scan leaves BUSY_PRS empty; if the in-flight leg already materialized and re-stamped the marker SUCCESS, the review-event scan re-dispatches the same PR — one wasted build-cli (~5 min) plus a cancelled queued sibling via latest-wins, the exact duplicate this PR closes for scheduled scans. Bounded, but on a path the PR title claims to close.

Suggested fix — narrow the exemption to genuine explicit dispatches; in-file precedent exists (the cap-refused gate ~line 3365 already splits on EVENT_NAME):

if [[ -z "${FORCED_PR}" || "${EVENT_NAME}" != 'workflow_dispatch' ]]; then
  CANDIDATES=''
fi

(or document in the FAIL-CLOSED comment that event-driven scans deliberately remain fail-open)

中文说明

【建议】 fail-closed 豁免比其声明的理由更宽:FORCED_PR 对每个受信任的 pull_request_review scan 同样会被设置(route 会为它们输出 pr_number;约 3330 行的 job 级注释明确写道 FORCED_PR "is ALSO set for every trusted pull_request_review … which is not an explicit dispatch"),因此事件驱动的 review scan 在枚举失败时仍是 fail-open,而新增的 FAIL-CLOSED 注释声称任何枚举失败都会清空本轮 scan 的候选集——失败场景:受信任 review 事件 scan 期间发生枚举失败(限流/5xx——正是 2026-08-16 产生重复派发的条件),BUSY_PRS 为空;若在途 leg 已物化并把标记重打为 SUCCESS,review 事件 scan 会重复派发同一 PR——浪费一次 build-cli(约 5 分钟)并经 latest-wins 取消排队的兄弟 leg,正是本 PR 为定时 scan 消除的那种重复。有界,但发生在 PR 标题声称要关闭的路径上。

建议修复:把豁免收窄到真正的显式派发;文件内已有先例(约 3365 行的 cap 拒绝门已经用 EVENT_NAME 做了同样的拆分):if [[ -z "${FORCED_PR}" || "${EVENT_NAME}" != 'workflow_dispatch' ]]; then CANDIDATES=''; fi(或在 FAIL-CLOSED 注释中说明事件驱动 scan 有意保持 fail-open)。

— qwen3.8-max via Qwen Code /review (v0.21.13)

Comment thread .github/workflows/qwen-autofix.yml Outdated
done <<< "$(sort -u <<< "${LIVE_RUNS}")"
fi
if [[ "${BUSY_ENUM_OK}" != '1' ]]; then
echo "::warning::busy-PR enumeration failed (run list or jobs view unreadable) — failing closed: no scan targets dispatched this pass"

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.

[Suggestion] Fail-closed emptying emits has_targets=false with exit 0, which downstream is indistinguishable from "no PR needs work" — a transient enumeration failure can flip the scheduled issue phase on against the workflow's declared ordering ("issue phase only if no PR needs work") — Failure scenario: on a schedule run, gh run list/gh run view fails; CANDIDATES=''TARGETS=[]has_targets=false, but the step still exits 0, so issue-autofix's clause needs.review-scan.result == 'success' && needs.review-scan.outputs.has_targets != 'true' (~line 753) passes and route sets DO_ISSUE=true on schedule — the bot claims a ready issue and starts an up-to-180-minute agent run while PRs sit with unaddressed review feedback. The old fail-open code never emptied CANDIDATES, so enumeration failure previously still yielded has_targets=true and suppressed the issue phase; this diff introduces the inversion.

Witness (probe of the verbatim extracted scan step, stubbed gh, schedule env):

fail arm (gh run list exit 1): ::warning, exit code 0, targets=[], has_targets=false
ok arm (same canned fleet):      has_targets=true, targets=[{"pr":"101",...}], PENDING stamped
-> flips on enumeration health alone

Suggested fix — emit a distinct signal on the fail-closed branch and gate the issue phase on it:

# next to the warning:
echo "enum_failed=true" >> "${GITHUB_OUTPUT}"
# and in issue-autofix's if: (and its concurrency predicate ~line 793):
&& needs.review-scan.outputs.enum_failed != 'true'

(or exit 1 on enumeration failure, which the existing result == 'success' conjunct already treats as issue-phase-suppressing)

中文说明

【建议】 fail-closed 清空候选集会以 exit 0 输出 has_targets=false,下游无法区分它与 "没有 PR 需要处理"——一次瞬时枚举失败可能把定时 issue 阶段翻转开启,违反工作流声明的顺序("issue phase only if no PR needs work")——失败场景:schedule run 中 gh run list/gh run view 失败;CANDIDATES=''TARGETS=[]has_targets=false,但 step 仍以 0 退出,于是 issue-autofix 的条件 needs.review-scan.result == 'success' && needs.review-scan.outputs.has_targets != 'true'(约 753 行)通过,route 在 schedule 上置 DO_ISSUE=true——bot 认领一个 ready issue 并启动最长 180 分钟的 agent run,而 PR 的 review 反馈无人处理。旧的 fail-open 代码从不清空 CANDIDATES,枚举失败时仍会得到 has_targets=true 并抑制 issue 阶段;这个反转是本 diff 引入的。

证据(对原样提取的 scan step 做探针,stub gh、schedule 环境):失败臂(gh run list exit 1)→ ::warning、exit 0、targets=[]、has_targets=false;正常臂(同一模拟 fleet)→ has_targets=true、targets=[{"pr":"101",...}]、打上 PENDING——仅随枚举健康状态翻转。

建议修复:在 fail-closed 分支输出独立信号并让 issue 阶段对其设门:在 warning 旁 echo "enum_failed=true" >> "${GITHUB_OUTPUT}",并在 issue-autofixif:(及约 793 行的并发谓词)中加 && needs.review-scan.outputs.enum_failed != 'true'(或在枚举失败时 exit 1,既有的 result == 'success' 合取项已把它视为抑制 issue 阶段)。

— qwen3.8-max via Qwen Code /review (v0.21.13)

Comment thread .github/workflows/qwen-autofix.yml Outdated
Comment on lines +4640 to +4641
gh api "repos/${REPO}/statuses/${CHECKED_OUT_HEAD}" -X POST \
-f state="success" -f context="${DISPATCH_STATUS_CONTEXT}" \

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.

[Suggestion] The release stamp runs only after checkout; the prepare step's three eligibility-discard exits (4568 INELIGIBLE ladder, 4596 fork fetch failure, 4620 fork push preflight) all precede it, and a repo-wide grep shows exactly two statuses/ call sites (3698 stamp, 4640 release) — so every deliberate same-repo discard strands the dispatch-pending marker at PENDING for the full 30-minute TTL with no leg in existence — Failure scenario: a scan dispatches a same-repo PR and stamps PENDING; while the leg sits queued, a transient gh pr view API error hits the address-time recheck (PR_LIVE='{}' → INELIGIBLE "metadata fetch failed (API error) — fail-closed"), or a maintainer applies autofix/skip, or the base/branch changes. Prepare exits 0 before checkout, nothing releases the marker, and the next scan's marker check skips the still-valid PR as "dispatch pending" for up to 30 minutes — contradicting the discard path's own documented promise at 4511-4513 ("fail closed — the next scan re-emits a still-valid target"). Until the Critical above is fixed, the same stranded marker additionally blocks via HAS_PENDING_CHECKS for up to ~330 minutes.

Witness: not run — cross-run timed scenario against live GitHub state; settled by complete static enumeration: exits 4568/4596/4620 all precede the only release site (4640); grep 'repos/${REPO}/statuses/' across all workflows returns only 3698 (stamp) and 4640 (release); the discard-path re-emit promise sits at 4511-4513.

Suggested fix — release on the discard path too: add headRefOid to the PR_LIVE --json list and, before the INELIGIBLE exit 0, best-effort stamp success on it for same-repo heads (guard with LIVE_XREPO == "false", mirroring the dispatch-side HEAD_REPO_FULL == REPO guard, || true like this site).

中文说明

【建议】 release 重打只在 checkout 之后运行;prepare step 的三个资格丢弃出口(4568 INELIGIBLE 阶梯、4596 fork fetch 失败、4620 fork push 预检)都在它之前,且全仓库 grep 显示 statuses/ 恰有两个调用点(3698 stamp、4640 release)——因此每一次同仓库的主动丢弃都会把 dispatch-pending 标记滞留在 PENDING 状态,持续整个 30 分钟 TTL,且不存在任何 leg——失败场景:scan 派发一个同仓库 PR 并打上 PENDING;leg 排队期间,一次瞬时的 gh pr view API 错误命中 address-time 复查(PR_LIVE='{}' → INELIGIBLE "metadata fetch failed (API error) — fail-closed"),或维护者打上 autofix/skip,或 base/分支变化。prepare 在 checkout 前以 0 退出,没有任何东西释放标记,下一轮 scan 的标记检查会把仍然有效的 PR 以 "dispatch pending" 跳过最长 30 分钟——与丢弃路径自己在 4511-4513 行文档化的承诺("fail closed — the next scan re-emits a still-valid target")相矛盾。在上面的 Critical 修复之前,同一滞留标记还会经 HAS_PENDING_CHECKS 额外阻塞最长约 330 分钟。

证据:未运行——跨 run 的定时场景依赖 live GitHub 状态;以完全静态枚举定案:出口 4568/4596/4620 全部位于唯一 release 点(4640)之前;跨所有工作流 grep repos/${REPO}/statuses/ 只返回 3698(stamp)与 4640(release);丢弃路径的"下一轮重新发出"承诺在 4511-4513 行。

建议修复:丢弃路径也释放标记——把 headRefOid 加进 PR_LIVE--json 列表,在 INELIGIBLE exit 0 之前对同仓库 head 尽力重打 success(用 LIVE_XREPO == "false" 做守卫,镜像 dispatch 侧的 HEAD_REPO_FULL == REPO 守卫,|| true 同此处)。

— qwen3.8-max via Qwen Code /review (v0.21.13)

Comment thread .github/workflows/qwen-autofix.yml Outdated
Comment on lines +3697 to +3698
if [[ "${HEAD_REPO_FULL}" == "${REPO}" ]]; then
gh api "repos/${REPO}/statuses/${LIVE_HEAD}" -X POST \

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.

[Suggestion] Both new commit-status writes bypass the file's own DRY_RUN write discipline: a workflow_dispatch with dry_run: true stamps real, PR-visible PENDING statuses. Every other write in the scan step is DRY_RUN-guarded (forced-takeover blocked report ~2476, deferred-review ack ~3094, cap-paused notice/label ~3436, stale-base update ~3549) — Failure scenario: a maintainer fires a dry-run dispatch to observe the loop; the scan stamps a real PENDING status on every selected same-repo head. Normally the dry-run leg's prepare step releases it — but review-address needs build-cli without always(), so if build-cli fails on the dry run, all legs are skipped and nothing ever releases the stamps: real scans then skip those PRs for ≤30 min via the marker check and, via the gate interaction reported above, up to ~330 min — a test mode that was supposed to touch nothing blocks real dispatches for hours.

Witness: not run — the load-bearing residual is Actions' needs-failure skip semantics and live commit-status effects; verified statically at the reviewed commit: review-address if: is has_targets == 'true' only (no always()), the two statuses/ writes are the only ones in the repo, and every comparable write in the scan step carries a DRY_RUN guard.

Suggested fix — guard both stamps:

if [[ "${DRY_RUN}" != "true" && "${HEAD_REPO_FULL}" == "${REPO}" ]]; then

(the prepare step's env currently lacks DRY_RUN — add DRY_RUN: '${{ needs.route.outputs.dry_run }}' there; skipping the stamp in dry runs degrades duplicate protection to the pre-PR baseline the comment below names as the surviving fallback)

中文说明

【建议】 两处新的 commit-status 写入都绕过了文件自身的 DRY_RUN 写入纪律:dry_run: trueworkflow_dispatch 会打上真实、PR 可见的 PENDING status。scan step 的其他所有写入都有 DRY_RUN 守卫(forced-takeover 拦截上报约 2476、deferred-review ack 约 3094、cap 暂停通知/标签约 3436、过期 base 更新约 3549)——失败场景:维护者发起 dry-run 派发以观察循环;scan 会给每个选中的同仓库 head 打上真实 PENDING status。通常 dry-run leg 的 prepare step 会释放它——但 review-address 依赖 build-cli 且没有 always(),所以 dry run 中 build-cli 一旦失败,所有 leg 都被跳过,没有任何东西释放这些 stamp:真实 scan 随后会经标记检查跳过这些 PR ≤30 分钟,再经上面报告的 gate 交互最长约 330 分钟——一个本应不产生任何副作用的测试模式阻塞真实派发长达数小时。

证据:未运行——承重残留是 Actions 的 needs 失败跳过语义与 live commit-status 效果;在受审 commit 上静态验证:review-addressif: 仅有 has_targets == 'true'(无 always()),两处 statuses/ 写入是全仓库仅有的,scan step 中所有可比写入都带 DRY_RUN 守卫。

建议修复:给两处 stamp 加守卫 if [[ "${DRY_RUN}" != "true" && "${HEAD_REPO_FULL}" == "${REPO}" ]]; then(prepare step 的 env 目前没有 DRY_RUN——需加 DRY_RUN: '${{ needs.route.outputs.dry_run }}';dry run 中不打 stamp 会把去重保护降级为下方注释所列的 PR 前基线兜底)。

— qwen3.8-max via Qwen Code /review (v0.21.13)

# already in PR_META. Unlike the in-memory busy skip this runs
# after the metadata fetch, so it consumes inspection budget;
# acceptable because the case is rare (a PR dispatched <30m ago).
if jq -e --arg ctx "${DISPATCH_STATUS_CONTEXT}" --arg cut "${DISPATCH_CUTOFF}" '

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.

[Suggestion] The dispatch-pending marker mechanism — this scan-side skip predicate, the PENDING stamp (~3698), and the SUCCESS re-stamp (~4640) — is pinned by nothing; no test references the context, the TTL, or the statuses/ endpoint (part of the same coverage gap as the comment on the enumeration block above) — Failure scenario: concrete surviving mutations: flipping > $cut to < $cut makes fresh markers pass and stale markers block, silently re-opening the duplicate-dispatch window this PR closes; a stamp/check/release context mismatch leaves the marker permanently unread; the suite pins the same-shaped comparison in the check-staleness filter explicitly because "a flipped comparison … is caught, not just a removed constant", but not here.

Suggested fix — replay the jq predicate over fixture rollups (fresh PENDING → skip; stale PENDING / SUCCESS / missing rollup → pass) and string-pin both stamp calls' state=/context= arguments against DISPATCH_STATUS_CONTEXT so writer/reader identity agreement fails in CI, not in production (the fork-bridge test uses exactly this cross-site pinning pattern).

中文说明

【建议】 dispatch-pending 标记机制——scan 侧这个跳过谓词、PENDING stamp(约 3698)、SUCCESS 重打(约 4640)——没有任何固定;测试中零处引用该 context、TTL 或 statuses/ 端点(与上方枚举块的覆盖缺口同属一处)——失败场景:可存活的具体变异——把 > $cut 翻成 < $cut 会让新鲜标记放行、过期标记阻塞,悄悄重开本 PR 要关闭的重复派发窗口;stamp/check/release 三方 context 不一致会让标记永远无人读取;套件对 check 过期过滤器里同形的比较特意做了固定,理由正是"翻转的比较也要能被抓到,而不只是删常量",但这里没有。

建议修复:用夹具 rollup 回放该 jq 谓词(新鲜 PENDING → 跳过;过期 PENDING / SUCCESS / rollup 缺失 → 放行),并把两处 stamp 调用的 state=/context= 参数与 DISPATCH_STATUS_CONTEXT 做字符串互锁,让写入方/读取方一致性在 CI 里失败而不是在生产里失败(fork-bridge 测试正是这种跨点互锁模式的现成例子)。

— qwen3.8-max via Qwen Code /review (v0.21.13)

Comment thread .github/workflows/qwen-autofix.yml Outdated
Comment on lines +4642 to +4643
-f description="address leg started (run ${GITHUB_RUN_ID})" \
> /dev/null 2>&1 || true

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.

[Suggestion] Sibling asymmetry: the dispatch-time stamp guards same-repo heads (HEAD_REPO_FULL == REPO), but this leg-side SUCCESS re-stamp — which builds the same API call — carries no equivalent guard, even though the fork idiom exists ~60 lines above ([[ "${HEAD_REPO:-${REPO}}" != "${REPO}" ]] at ~4579) and HEAD_REPO is in scope in this step — Failure scenario: fork legs are reachable (forced fork admission ~2621; scheduled-scan admission of fork PRs). On a fork leg, CHECKED_OUT_HEAD came from the fork, so the POST targets a sha absent from the base repo's object store → 422 on every fork leg, swallowed by || true (one wasted API call each), contradicting the invariant the stamp side documents ("Same-repo heads only get a stamp"). If the fork head commit does exist in the base object store, it instead paints SUCCESS for a context never stamped PENDING there.

Suggested fix — mirror the stamp side:

if [[ "${HEAD_REPO:-${REPO}}" == "${REPO}" ]]; then
  gh api "repos/${REPO}/statuses/${CHECKED_OUT_HEAD}" -X POST ...
fi
中文说明

【建议】 兄弟不对称:dispatch 侧的 stamp 有同仓库守卫(HEAD_REPO_FULL == REPO),但 leg 侧这个 SUCCESS 重打——构造的是同一个 API 调用——没有等价守卫,而 fork 惯用写法就在约 60 行上方(约 4579 行的 [[ "${HEAD_REPO:-${REPO}}" != "${REPO}" ]]),且 HEAD_REPO 在本 step 作用域内——失败场景:fork leg 可达(forced fork 准入约 2621;定时 scan 对 fork PR 的准入)。fork leg 上 CHECKED_OUT_HEAD 来自 fork,POST 指向 base 仓库对象库中不存在的 sha → 每个 fork leg 都 422,被 || true 吞掉(每次浪费一个 API 调用),与 stamp 侧文档化的不变量("Same-repo heads only get a stamp")矛盾。若 fork head commit 恰存在于 base 对象库,则会为一个从未在该 sha 上打过 PENDING 的 context 涂上 SUCCESS。

建议修复:镜像 stamp 侧——if [[ "${HEAD_REPO:-${REPO}}" == "${REPO}" ]]; then gh api ... fi

— qwen3.8-max via Qwen Code /review (v0.21.13)

if [[ "${BUSY_ENUM_OK}" == '1' ]]; then
while IFS= read -r LIVE_RUN; do
[[ -z "${LIVE_RUN}" ]] && continue
if ! BUSY_OUT="$(gh run view "${LIVE_RUN}" --repo "${REPO}" --json jobs \

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.

[Suggestion] The busy-set enumeration this diff rewrites exists near-verbatim in .github/workflows/qwen-fleet-shepherd.yml (~lines 297-323, SHEP_BUSY/BUSY_OK): identical capture("^review-address \\((?<pr>[0-9]+),") regex, identical accumulation into a space-delimited busy string, already fail-closed — now two divergently wrapped copies of the same busy definition (the shepherd copy has no knowledge of the new dispatch-pending window) — Failure scenario: a rename/reformat of the review-address matrix job naming, or a busy-definition change, fixed on one side only silently empties the other side's busy set → re-dispatch of a PR already being addressed, the exact duplicate-build-cli cost (~5 min each) this PR exists to eliminate.

Suggested fix: extract the enumeration into a shared script (e.g. .github/scripts/list-busy-autofix-prs.sh) called from both workflows, or at minimum add cross-referencing comments at both sites so a regex or semantics change lands in both.

中文说明

【建议】 本 diff 重写的 busy 集合枚举在 .github/workflows/qwen-fleet-shepherd.yml(约 297-323 行,SHEP_BUSY/BUSY_OK)里有近乎逐字的副本:相同的 capture("^review-address \\((?<pr>[0-9]+),") 正则、相同的空格分隔 busy 串累积、且已是 fail-closed——现在同一 busy 定义有了两个包装方式已经分叉的副本(shepherd 副本不知道新的 dispatch-pending 窗口)——失败场景:review-address matrix job 命名被改名/改格式,或 busy 定义变更,只修一侧会悄悄清空另一侧的 busy 集合 → 重复派发正在处理中的 PR,正是本 PR 要消除的重复 build-cli 成本(每次约 5 分钟)。

建议修复:把枚举抽成共享脚本(如 .github/scripts/list-busy-autofix-prs.sh)供两个工作流调用,或至少在两处加交叉引用注释,确保正则或语义变更同时落到两侧。

— qwen3.8-max via Qwen Code /review (v0.21.13)

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.

Verified and deferred to the follow-up queue — the duplication is real: .github/workflows/qwen-fleet-shepherd.yml (~lines 297-323) carries the same capture("^review-address \\((?<pr>[0-9]+),") busy-set enumeration, already fail-closed via BUSY_OK, and it does not know about the new dispatch-pending window. Both suggested fixes (extracting a shared .github/scripts/list-busy-autofix-prs.sh, or adding cross-referencing comments at both sites) require touching the shepherd workflow and/or creating a new shared script — areas this PR has never touched, so implementing them here would expand the round outside the PR's footprint. The follow-up issue tracks it; the thread stays open.

中文说明

已核实并推迟到后续跟进队列——重复属实:.github/workflows/qwen-fleet-shepherd.yml(约 297-323 行)携带相同的 capture("^review-address \\((?<pr>[0-9]+),") busy 集枚举,已通过 BUSY_OK fail-closed,但不知道新的 dispatch-pending 窗口。两种建议修复(抽取共享的 .github/scripts/list-busy-autofix-prs.sh,或在两处加交叉引用注释)都需要触碰 shepherd 工作流和/或新建共享脚本——均为本 PR 从未触碰的区域,在此实现会把本轮改动扩出 PR 足迹。跟进 issue 会跟踪此项;线程保持打开。

Comment thread .github/workflows/qwen-autofix.yml Outdated
while IFS= read -r LIVE_RUN; do
[[ -z "${LIVE_RUN}" ]] && continue
if ! BUSY_OUT="$(gh run view "${LIVE_RUN}" --repo "${REPO}" --json jobs \
--jq '.jobs[] | select(.status != "completed") | .name | capture("^review-address \\((?<pr>[0-9]+),") | .pr' 2> /dev/null)"; then

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.

[Suggestion] The fail-closed path discards every underlying error detail — both enumeration calls redirect 2> /dev/null — so the warning below cannot say which call failed or why. Transient API instability and PAT degradation produce identical opaque log output and need different responses (wait a tick vs fix the token) — Failure scenario: in a persistent-failure case (PAT expiry/revoked scope, sustained Actions API outage), every scan silently stops dispatching behind the same fixed warning; the oncall responder seeing "autofix stopped working" has no error text in the job log and must manually re-run gh run list / gh run view on the runner to discover the cause.

Suggested fix — capture stderr instead of discarding it (in-file precedent: the route job captures API errors via api_error_file="$(mktemp)" ~line 497 and includes them in its warning), and append the tail to the ::warning:: and fleet_row detail.

中文说明

【建议】 fail-closed 路径丢弃了全部底层错误细节——两处枚举调用都 2> /dev/null——因此下方的 warning 说不出是哪个调用失败、为何失败。瞬时 API 抖动与 PAT 失效产生完全相同的不透明日志,却需要不同的处置(等一轮 tick vs 修 token)——失败场景:持续失败时(PAT 过期/权限被收回、Actions API 持续故障),每轮 scan 都静默停止派发、只留同一句固定 warning;看到 "autofix 不工作了" 的 oncall 在 job 日志里找不到任何错误文本,只能上 runner 手工重跑 gh run list / gh run view 找原因。

建议修复:捕获 stderr 而不是丢弃(文件内先例:route job 约 497 行用 api_error_file="$(mktemp)" 捕获 API 错误并写进 warning),把尾部追加进 ::warning::fleet_row 详情。

— qwen3.8-max via Qwen Code /review (v0.21.13)

# already drops targets whose PR closed after dispatch.
if: |-
${{ github.repository == 'QwenLM/qwen-code' && (github.event_name != 'issue_comment' || (github.event.issue.pull_request && (startsWith(github.event.comment.body, '@qwen-code /takeover') || startsWith(github.event.comment.body, '@qwen-code /retry')))) && (github.event_name != 'pull_request' || github.event.label.name == 'autofix/takeover') }}
${{ github.repository == 'QwenLM/qwen-code' && (github.event_name != 'issue_comment' || (github.event.issue.pull_request && (startsWith(github.event.comment.body, '@qwen-code /takeover') || startsWith(github.event.comment.body, '@qwen-code /retry')))) && (github.event_name != 'pull_request' || github.event.label.name == 'autofix/takeover') && (github.event_name != 'pull_request_review' || github.event.pull_request.state == 'open') }}

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.

[Suggestion] This gate-clause hunk re-implements the P0 route gate already merged into main via PR #9299 (commit 5492009bb2): the branch forked at b744248656 before that merge and carries its own byte-identical copy as commit 3a09e73fad (verified: git show of both commits produces patch-identical changes; main is 24 commits ahead; git merge-tree resolves cleanly). The PR description says "this PR is the busy-detection hardening only" and does not mention the gate — Failure scenario: reviewers spend attention on ~13 already-shipped lines in Files changed, and the branch sits 24 commits behind main. The merge itself absorbs the duplicate cleanly, so the cost is pre-merge: diff noise and misdescribed scope.

Suggested fix: rebase the branch onto current main — patch-id identity drops 3a09e73fad, leaving a 1-commit PR containing only the busy-detection changes.

中文说明

【建议】 这个门子句 hunk 重复实现了已经通过 PR #9299(commit 5492009bb2)合入 main 的 P0 route 门:分支在 b744248656 处分叉,早于那次合并,并以 commit 3a09e73fad 携带了一份字节级相同的副本(已验证:两个 commit 的 git show 补丁完全一致;main 领先 24 个 commit;git merge-tree 干净合并)。PR 描述写的是 "this PR is the busy-detection hardening only",并未提及这个门——失败场景:reviewer 要在 Files changed 里花注意力看约 13 行已发布的内容,且分支落后 main 24 个 commit。合并本身能干净吸收这份重复,所以成本在合并前:diff 噪音与失实的范围描述。

建议修复:把分支 rebase 到当前 main——patch-id 相同会自动丢掉 3a09e73fad,得到一个只含 busy 检测改动的单 commit PR。

— qwen3.8-max via Qwen Code /review (v0.21.13)

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.

Declined with evidence. The underlying facts are verified: 3a09e73fad on this branch and 5492009bb2 on main (via #9299) share patch-id 5d0a0dba5db3a618cdbe6d3128f42ae7f878dbf8, and git merge-tree --write-tree origin/main HEAD resolves cleanly. But the suggested rebase is history rewriting, which this headless autofix flow forbids (additive commits only). The additive alternative — merging main into the branch — is discouraged when there is no conflict to resolve, and would pull ~32 unrelated commits into a live CI workflow branch to remove ~13 lines of diff noise that the eventual merge absorbs cleanly (patch-id identity drops the duplicate at squash/merge time). The residual cost is pre-merge reviewer attention on already-shipped lines; the PR-description scope note is owned by the workflow's PR-preparation step, which this round cannot rewrite either.

中文说明

以证据拒绝。 底层事实已核实:本分支的 3a09e73fad 与 main 上的 5492009bb2(经 #9299)共享 patch-id 5d0a0dba5db3a618cdbe6d3128f42ae7f878dbf8,且 git merge-tree --write-tree origin/main HEAD 干净合并。但建议的 rebase 属于历史重写,本 headless autofix 流程禁止该操作(只允许追加式提交)。追加式替代方案——把 main 合并进分支——在没有冲突需要解决时不被鼓励,且会把约 32 个无关提交拉进一条生产 CI 工作流分支,只为去掉约 13 行最终合并会干净吸收的 diff 噪音(patch-id 相同会在 squash/merge 时自动丢掉重复提交)。残留成本仅是 reviewer 在合并前要在已发布行上花的注意力;PR 描述的范围说明由工作流的 PR 准备步骤负责,本轮同样无法改写。

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 2 finishedview run. See this round's report below.

中文说明

AutoFix 第 2 轮已完成 —— 查看运行。本轮报告见下方。

Exempt the marker's status context from the HAS_PENDING_CHECKS gate (a
stranded marker otherwise blocked the PR for up to ~330 minutes, not the
documented 30-minute TTL), release it on the address-time discard path,
guard every status write same-repo and dry-run, narrow the fail-closed
carve-out to explicit workflow_dispatch dispatches, emit enum_failed so
an emptied candidate set cannot flip the scheduled issue phase on, and
carry the enumeration error tail in the fail-closed warning. Pin all of
it behaviorally in the workflow contract tests.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round — PR #9329

Round outcome: 1 Critical + 9 suggestions implemented in code, 1 suggestion deferred to the follow-up queue, 1 suggestion declined with evidence. One commit: 9dbaff5872 fix(ci): keep the dispatch-pending marker from blocking past its TTL. No base conflicts (--conflict false, no merge performed).

Feedback points and dispositions

Critical (resolved in code)

  • [rc:3795382505] Stranded dispatch-pending marker blocks via HAS_PENDING_CHECKS for up to ~330 min instead of the documented 30-min TTL — FIXED.
    Reproduced first: running the workflow's exact HAS_PENDING_CHECKS jq from the pre-round commit over a fixture rollup containing only a 60-minute-old qwen-autofix/dispatch-pending StatusContext returned true (blocked), confirming a stranded marker keeps every scan skipping the PR as "active checks in flight" long after the marker's own TTL check passes. Fix: the gate now binds --arg ctx "${DISPATCH_STATUS_CONTEXT}" and exempts (.context // "") == $ctx — the marker is this loop's own busy signal, and its dedicated 30-minute TTL check remains the authority on it. Verified across six fixture classes: stranded marker alone no longer blocks; genuine in-flight checks still block (alone and alongside the marker); the 330-minute aging horizon still applies to real checks; foreign status contexts still block; SUCCESS markers never block. Pinned behaviorally in a new contract test that FAILS against the pre-round branch (returned true where it asserts false).

Suggestions implemented

  • [rc:3795382508] Fail-closed exemption broader than its justification — FIXED. The carve-out is now [[ -z "${FORCED_PR}" || "${EVENT_NAME}" != 'workflow_dispatch' ]], matching the cap-refused gate's existing EVENT_NAME split: trusted pull_request_review scans (which also carry FORCED_PR) now fail closed on enumeration failure like scheduled scans; only explicit dispatches keep their override semantics. The FAIL-CLOSED comment was updated to say exactly this. Behaviorally replayed: a review-event scan with FORCED_PR empties its candidates on enumeration failure; a workflow_dispatch scan keeps them.
  • [rc:3795382512] Fail-closed emptying indistinguishable from "no PR needs work" — FIXED. The scan now emits enum_failed=true (declared as a review-scan job output), and both the issue-autofix if: and its concurrency-group predicate gained && needs.review-scan.outputs.enum_failed != 'true' inside their existing schedule-only clauses. An emptied-by-failure candidate set can no longer flip the scheduled issue phase on; a genuinely empty set still does. The equivalence pin between the if: and the group expression was updated on both sides and still passes.
  • [rc:3795382519] Deliberate same-repo discards strand the marker for the full TTL — FIXED. PR_LIVE now fetches headRefOid; the INELIGIBLE discard branch releases the marker with a best-effort success stamp on the live head, guarded by DRY_RUN != true, LIVE_XREPO == false (fork heads were never stamped), and a non-empty sha. The other two discard exits (fork fetch / fork push preflight) are only reachable for fork legs, which carry no stamp, so one release point suffices. Behaviorally replayed over the recheck block: same-repo discards stamp state=success + the marker context; fork discards, dry runs, and missing-sha discards write nothing; healthy targets write nothing (the checkout-path release owns that stamp).
  • [rc:3795382521] Both commit-status writes bypassed the DRY_RUN discipline — FIXED. The dispatch-time stamp is now guarded [[ "${DRY_RUN}" != "true" && "${HEAD_REPO_FULL}" == "${REPO}" ]]; the prepare step's env gained DRY_RUN: '${{ needs.route.outputs.dry_run }}', and both leg-side stamps (checkout release, discard release) carry the same dry-run guard. A dry run whose build-cli fails can no longer strand a real PENDING status that blocks real scans.
  • [rc:3795382525] Stale "fail-open, like BUSY_PRS" analogy — FIXED. The REVIEW_WF_ID comment now reads "(fail-open here, though the BUSY_PRS enumeration below is fail-closed)".
  • [rc:3795382530] Fail-closed enumeration pinned by no test — FIXED. New contract test extracts the enumeration block verbatim and replays it with a stubbed gh that emulates the real CLI contract (applies --jq like real gh): run-list failure empties candidates and emits enum_failed; jobs-view failure mid-enumeration fails closed the same way; a healthy enumeration accumulates the busy set and keeps candidates; the explicit-dispatch carve-out and the review-event fail-closed arm are both replayed.
  • [rc:3795382538] Marker mechanism pinned by nothing — FIXED. New lifecycle test pins the context constant, the TTL, the cutoff construction, and the freshness comparison direction, replays the skip predicate over five fixture rollups (fresh PENDING → skip; stale PENDING / SUCCESS / foreign context / empty → pass), replays the HAS_PENDING_CHECKS exemption (the Critical's regression), and cross-site-pins writer/reader identity: all three statuses/ writes bind ${DISPATCH_STATUS_CONTEXT} (count-pinned to 3) and both readers bind it via --arg ctx (count-pinned to 2), plus the same-repo + dry-run guards on every write site.
  • [rc:3795382543] Leg-side SUCCESS re-stamp missing the same-repo guard — FIXED. The re-stamp is now wrapped in [[ "${DRY_RUN}" != "true" && "${HEAD_REPO:-${REPO}}" == "${REPO}" ]], mirroring the dispatch-side guard with the fork idiom already used ~60 lines above; no more 422-per-fork-leg waste or SUCCESS painted for a never-stamped context.
  • [rc:3795382556] Fail-closed path discarded all error detail — FIXED. Both enumeration calls now redirect stderr into a temp file (route-job api_error_file precedent); on failure the last 200 bytes are appended to the ::warning:: and the fleet row, so transient API instability and PAT decay no longer produce identical opaque output. The replay test asserts the error tail reaches the fleet row.

Suggestion deferred to the follow-up queue

  • [rc:3795382552] Busy-set duplication in qwen-fleet-shepherd.yml — DEFERRED. Verified real: the shepherd (~lines 297-323) carries the same capture("^review-address \\((?<pr>[0-9]+),") enumeration, already fail-closed via BUSY_OK, unaware of the new dispatch-pending window. Both suggested fixes (extract a shared script, or add cross-referencing comments at both sites) require touching .github/workflows/qwen-fleet-shepherd.yml and/or creating .github/scripts/list-busy-autofix-prs.sh — areas this PR has never touched, so implementing here would expand the round outside the PR's footprint. Recorded in deferred-findings.json; the thread stays open.

Suggestion declined with evidence

  • [rc:3795382559] Rebase onto main to drop the duplicate P0 gate commit — DECLINED. The underlying facts are verified: 3a09e73fad (this branch) and 5492009bb2 (main, via fix(ci): drop pull_request_review events on closed PRs at the route gate #9299) share patch-id 5d0a0dba…, and git merge-tree --write-tree origin/main HEAD resolves cleanly. But the suggested rebase is history rewriting, which this headless flow forbids (additive commits only). The additive alternative — merging main into the branch — is discouraged here (--conflict false: no conflict to resolve) and would pull ~32 unrelated commits into a live CI workflow branch to remove ~13 lines of diff noise that the eventual merge absorbs cleanly. Declined as not worth the blast radius within this flow's constraints.

Review body

  • [rv:4950414646] CHANGES_REQUESTED ("Partially reviewed — gaps disclosed") — the state rested on the Critical above, which is now fixed in code with a pre-round-failing regression test. The disclosed gaps (reverse audit not reached within the review time budget; actionlint embedded-shell source mapping unsupported for this file) are reviewer-side process/tool limitations with no code change available in this round.

Files changed

  • .github/workflows/qwen-autofix.yml — marker exemption in HAS_PENDING_CHECKS, marker release on the discard path, DRY_RUN + same-repo guards on all three status writes, narrowed fail-closed carve-out, enum_failed output + both issue-phase gates, stderr capture, comment fixes.
  • scripts/tests/qwen-autofix-workflow.test.js — three new behavioral replay/pin tests; two pre-existing replays updated to carry the new ctx binding the gate now references.

Verification

Commands actually run this round, in order:

  • Probe of the pre-round HAS_PENDING_CHECKS jq over a stranded-marker fixture — returned true (defect reproduced)
  • Probe of the post-fix jq over six fixture classes — all expected (false for the stranded marker alone; real checks, foreign contexts, and marker+check mixes still block; aged-out checks and SUCCESS markers do not)
  • bash -n on the extracted scan-step and prepare-step run blocks — both parse
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js177/177 passed (re-run after the commit, including the pre-commit hook's reformatting)
  • Pre-round regression proof: the new pending-gate test's replay run against git show HEAD~1:'s workflow returns true where the test asserts false — the test fails pre-round as the verification gate requires
  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0, no errors/warnings)
  • npx prettier --check scripts/tests/qwen-autofix-workflow.test.js — passed
  • Not run: integration tests (the change is GitHub Actions workflow YAML, exercised only by the CI runtime and the workflow contract suite above, never by the bundled CLI); npm run generate:settings-schema (no settings source touched). yamllint/actionlint binaries are not installed on this runner (CI provisions them); the touched YAML parses cleanly and both modified run blocks pass bash -n plus behavioral replay.
中文说明

Autofix 审查轮次 — PR #9329

本轮结果:1 个 Critical + 9 条建议已在代码中实现,1 条建议推迟到后续跟进队列,1 条建议以证据拒绝。 单个提交:9dbaff5872 fix(ci): keep the dispatch-pending marker from blocking past its TTL。无 base 冲突(--conflict false,未执行合并)。

反馈点与处置

Critical(已在代码中解决)

  • [rc:3795382505] 滞留的 dispatch-pending 标记会经 HAS_PENDING_CHECKS 阻塞最长约 330 分钟,而非文档承诺的 30 分钟 TTL —— 已修复。
    先复现:用本轮之前提交上的工作流原样 HAS_PENDING_CHECKS jq,对只含一个 60 分钟龄 qwen-autofix/dispatch-pending StatusContext 的夹具 rollup 运行,返回 true(阻塞),确认滞留标记会让每轮 scan 以 "active checks in flight" 跳过该 PR,远超过标记自身 TTL 检查放行的时间。修复:该门现在绑定 --arg ctx "${DISPATCH_STATUS_CONTEXT}" 并豁免 (.context // "") == $ctx —— 该标记是本循环自己的 busy 信号,其专属的 30 分钟 TTL 检查才是它的权威判定。已在六类夹具上验证:仅滞留标记不再阻塞;真实在途检查仍然阻塞(单独存在及与标记并存时都是);330 分钟老化阈值对真实检查仍然生效;外部 status context 仍然阻塞;SUCCESS 标记从不阻塞。以新的契约测试做行为级固定,该测试对本轮之前的分支会失败(返回 true 而断言要求 false)。

已实现的建议

  • [rc:3795382508] fail-closed 豁免比其声明的理由更宽 —— 已修复。 豁免条件现在是 [[ -z "${FORCED_PR}" || "${EVENT_NAME}" != 'workflow_dispatch' ]],与 cap 拒绝门既有的 EVENT_NAME 拆分一致:受信任的 pull_request_review scan(同样携带 FORCED_PR)现在与定时 scan 一样在枚举失败时 fail closed;只有显式派发保留其覆盖语义。FAIL-CLOSED 注释已同步更新。行为回放验证:携带 FORCED_PR 的 review 事件 scan 在枚举失败时清空候选集;workflow_dispatch scan 保留候选集。
  • [rc:3795382512] fail-closed 清空与 "没有 PR 需要处理" 无法区分 —— 已修复。 scan 现在输出 enum_failed=true(已声明为 review-scan 的 job output),issue-autofix 的 if: 与其并发组谓词都在既有的 schedule-only 子句中增加了 && needs.review-scan.outputs.enum_failed != 'true'。因失败而被清空的候选集不再能把定时 issue 阶段翻转开启;真正的空集合仍然可以。if: 与并发组表达式的等价固定已在两侧同步更新并通过。
  • [rc:3795382519] 同仓库的主动丢弃会让标记滞留整个 TTL —— 已修复。 PR_LIVE 现在获取 headRefOid;INELIGIBLE 丢弃分支会在 live head 上尽力重打 success 来释放标记,守卫为 DRY_RUN != trueLIVE_XREPO == false(fork head 从未被打过标记)与非空 sha。另外两个丢弃出口(fork fetch / fork push 预检)只有 fork leg 可达,而 fork 没有标记,因此一个释放点即可。对 recheck 块做了行为回放:同仓库丢弃会打 state=success + 标记 context;fork 丢弃、dry run、缺 sha 的丢弃不产生任何写入;健康目标也不写入(checkout 路径的释放负责那次 stamp)。
  • [rc:3795382521] 两处 commit-status 写入都绕过了 DRY_RUN 纪律 —— 已修复。 dispatch 侧 stamp 现在的守卫是 [[ "${DRY_RUN}" != "true" && "${HEAD_REPO_FULL}" == "${REPO}" ]];prepare step 的 env 增加了 DRY_RUN: '${{ needs.route.outputs.dry_run }}',两处 leg 侧 stamp(checkout 释放、丢弃释放)都带同样的 dry-run 守卫。build-cli 失败的 dry run 不再可能滞留一个阻塞真实 scan 的真实 PENDING status。
  • [rc:3795382525] 过期的 "fail-open, like BUSY_PRS" 类比 —— 已修复。 REVIEW_WF_ID 注释现在写作 "(fail-open here, though the BUSY_PRS enumeration below is fail-closed)"。
  • [rc:3795382530] fail-closed 枚举没有任何测试固定 —— 已修复。 新的契约测试逐字提取枚举块,并用模拟真实 CLI 契约(像真实 gh 一样应用 --jq)的 stub gh 回放:run-list 失败会清空候选集并输出 enum_failed;枚举中途 jobs-view 失败同样 fail closed;健康枚举仍累积 busy 集合并保留候选集;显式派发豁免与 review 事件 fail-closed 两臂都有回放。
  • [rc:3795382538] 标记机制没有任何固定 —— 已修复。 新的生命周期测试固定 context 常量、TTL、cutoff 构造与新鲜度比较方向,在五类夹具 rollup 上回放跳过谓词(新鲜 PENDING → 跳过;过期 PENDING / SUCCESS / 外部 context / 空 → 放行),回放 HAS_PENDING_CHECKS 豁免(即 Critical 的回归),并做跨点互锁:三处 statuses/ 写入全部绑定 ${DISPATCH_STATUS_CONTEXT}(数量固定为 3),两处读取都经 --arg ctx 绑定(数量固定为 2),且每个写入点都有同仓库 + dry-run 守卫。
  • [rc:3795382543] leg 侧 SUCCESS 重打缺少同仓库守卫 —— 已修复。 重打现在包裹在 [[ "${DRY_RUN}" != "true" && "${HEAD_REPO:-${REPO}}" == "${REPO}" ]] 中,与 dispatch 侧守卫镜像,使用约 60 行上方已有的 fork 惯用写法;不再在每个 fork leg 上浪费一次 422,也不会为一个从未打过 PENDING 的 context 涂上 SUCCESS。
  • [rc:3795382556] fail-closed 路径丢弃了全部错误细节 —— 已修复。 两处枚举调用的 stderr 现在重定向进临时文件(route job 的 api_error_file 先例);失败时最后 200 字节会追加进 ::warning:: 与 fleet 行,瞬时 API 抖动与 PAT 失效不再产生完全相同的不透明输出。回放测试断言错误尾部出现在 fleet 行中。

推迟到后续跟进队列的建议

  • [rc:3795382552] qwen-fleet-shepherd.yml 中的 busy 集重复 —— 已推迟。 已核实属实:shepherd(约 297-323 行)携带相同的 capture("^review-address \\((?<pr>[0-9]+),") 枚举,且已通过 BUSY_OK fail-closed,但不知道新的 dispatch-pending 窗口。两种建议修复(抽取共享脚本,或在两处加交叉引用注释)都需要触碰本 PR 从未触碰过的 .github/workflows/qwen-fleet-shepherd.yml 和/或新建 .github/scripts/list-busy-autofix-prs.sh,在本轮实现会把改动扩出 PR 足迹。已记录进 deferred-findings.json;该线程保持打开。

以证据拒绝的建议

  • [rc:3795382559] rebase 到 main 以丢弃重复的 P0 门提交 —— 已拒绝。 底层事实已核实:3a09e73fad(本分支)与 5492009bb2(main,经 fix(ci): drop pull_request_review events on closed PRs at the route gate #9299)共享 patch-id 5d0a0dba…,且 git merge-tree --write-tree origin/main HEAD 干净合并。但建议的 rebase 属于历史重写,本 headless 流程禁止(只允许追加式提交)。追加式替代方案——把 main 合并进分支——在此不被鼓励(--conflict false:没有需要解决的冲突),且会把约 32 个无关提交拉进一条生产 CI 工作流分支,只为去掉约 13 行最终合并会干净吸收的 diff 噪音。以"在本流程约束下不值得该影响面"为由拒绝。

审查正文

  • [rv:4950414646] CHANGES_REQUESTED("Partially reviewed — gaps disclosed") —— 该状态取决于上面的 Critical,现已在代码中修复,并配有对本轮之前分支失败的回归测试。已披露的缺口(反向审计在审查时间预算内未开始;actionlint 对本文件的 embedded-shell source mapping 不支持)是审查侧的流程/工具限制,本轮没有可做的代码改动。

变更文件

  • .github/workflows/qwen-autofix.yml —— HAS_PENDING_CHECKS 中的标记豁免、丢弃路径的标记释放、三处 status 写入的 DRY_RUN + 同仓库守卫、收窄的 fail-closed 豁免、enum_failed 输出与两处 issue 阶段门、stderr 捕获、注释修正。
  • scripts/tests/qwen-autofix-workflow.test.js —— 三个新的行为回放/固定测试;两个既有回放更新为携带该门现在引用的新 ctx 绑定。

验证

本轮实际执行的命令,按顺序:

  • 对本轮之前提交上的 HAS_PENDING_CHECKS jq 做滞留标记夹具探针 —— 返回 true(缺陷复现)
  • 对修复后的 jq 做六类夹具探针 —— 全部符合预期(仅滞留标记为 false;真实检查、外部 context、标记+检查混合仍阻塞;老化检查与 SUCCESS 标记不阻塞)
  • 对提取的 scan step 与 prepare step run 块执行 bash -n —— 均通过
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js —— 177/177 通过(在提交之后重跑,含 pre-commit 钩子的重新格式化)
  • 本轮之前的回归证明:新 pending-gate 测试的回放对 git show HEAD~1: 的工作流返回 true,而测试断言 false —— 测试在本轮之前失败,满足验证门要求
  • npm run build —— 通过(exit 0)
  • npm run typecheck —— 通过(exit 0)
  • npm run lint —— 通过(exit 0,无 error/warning)
  • npx prettier --check scripts/tests/qwen-autofix-workflow.test.js —— 通过
  • 未运行:集成测试(本次变更是 GitHub Actions 工作流 YAML,只由 CI 运行时与上述工作流契约套件执行,bundled CLI 从不执行);npm run generate:settings-schema(未触碰 settings 源)。yamllint/actionlint 二进制未安装在本 runner(由 CI 提供);触碰的 YAML 解析正常,两处修改的 run 块均通过 bash -n 与行为回放。

🧭 Gate advisory — this round modified areas outside the PR footprint (machine-measured, not agent-authored):

  • scripts
    Review the expansion deliberately; the footprint gate is in advisory mode. · 本轮改动了 PR 足迹之外的区域(门自动测量,非 agent 文本),当前足迹门为 advisory 模式,请有意识地审阅该扩张。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@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.

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 10.

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:

  • .github/workflows/qwen-autofix.yml:317 — [review] New route-gate open-PR clause is pinned by no test
  • .github/workflows/qwen-autofix.yml:2951 — [review] Marker check blocks the explicit workflow_dispatch override lane on a stranded marker
  • .github/workflows/qwen-autofix.yml:2951 — [review] Head move during the build-cli window leaves the new head's rollup marker-less (skip side)
中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查:反向审计——在 10 轮的反审轮数上限内未收敛。

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.21.13)

# skipped when build-cli fails), and a stranded real PENDING
# would then block real scans — duplicate protection degrades to
# those same surviving layers.
if [[ "${DRY_RUN}" != "true" && "${HEAD_REPO_FULL}" == "${REPO}" ]]; then

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.

[Suggestion] A stamped dispatch-pending status is never cleared on GitHub when the leg never materializes: review-address is needs: ['route', 'review-scan', 'build-cli'] with default success-gating, so a build-cli failure skips the leg entirely and neither release site (discard at ~4592, checkout at ~4678) ever runs; run cancellation or a prepare-step crash before checkout yields the same. Commit statuses do not expire, so the PR page shows qwen-autofix/dispatch-pending = pending indefinitely while the head sha is unchanged. Automation itself is unaffected after the 30-minute TTL (the reader and the HAS_PENDING_CHECKS exemption both age it out correctly), so this is PR-page hygiene and a misleading status, not dispatch correctness — but the stamp guard already mitigates the exact analogous dry-run case, leaving the non-dry variant unhandled. — Failure scenario: scan dispatches a same-repo PR and stamps PENDING; build-cli then fails (a path the stamp-site comment itself names); review-address is skipped, neither release site runs; humans and status-watching tooling see a permanent pending status announcing a leg that is never coming.

Suggested fix — re-stamp the dispatched targets' heads with a non-success state from a finalizer that runs when the leg does not materialize:

# e.g. a small job with `if: failure()` keyed on build-cli;
# the target list is already in needs.review-scan.outputs.targets
finalize-dispatch-markers:
  needs: ['review-scan', 'build-cli']
  if: ${{ failure() && needs.review-scan.outputs.has_targets == 'true' }}
  # for each target: gh api repos/${REPO}/statuses/${head} -X POST
  #   -f state="error" -f context="${DISPATCH_STATUS_CONTEXT}"
  #   -f description="dispatch did not materialize (run ...)"
中文说明

【建议】 当 leg 始终没有物化时,已打上的 dispatch-pending status 在 GitHub 上永远不会被清除:review-addressneeds: ['route', 'review-scan', 'build-cli'] 且默认 success 门控,因此 build-cli 失败会整体跳过 leg,两个释放点(~4592 的丢弃释放、~4678 的 checkout 释放)都不会执行;run 被取消或 prepare 步骤在 checkout 前崩溃同样如此。commit status 不会过期,因此在 head sha 不变时 PR 页面会无限期显示 qwen-autofix/dispatch-pending = pending。30 分钟 TTL 之后自动化本身不受影响(读取端与 HAS_PENDING_CHECKS 豁免都会按年龄放行),所以这是 PR 页面卫生与误导性状态问题,而非派发正确性问题——但 stamp 守卫已经为完全类似的 dry-run 情形做了缓解,非 dry-run 变体却没有处理。失败场景:scan 派发一个同仓库 PR 并打上 PENDING;随后 build-cli 失败(stamp 处注释自己点名的路径);review-address 被跳过,两个释放点都不执行;人类与关注 status 的工具会看到一个永久的 pending 状态,宣称一个永远不会到来的 leg。

建议修复:用一个在 leg 未物化时运行的收尾 job,把已派发 target 的 head 重打为非 success 状态(例如 if: failure() 挂在 build-cli 上的小 job;target 列表已经在 needs.review-scan.outputs.targets 中)。

— qwen3.8-max via Qwen Code /review (v0.21.13)

# pending for the full TTL with no leg coming — against the
# re-emit promise above. Same-repo heads only (fork heads were
# never stamped) and never in a dry run (nothing was stamped).
if [[ "${DRY_RUN}" != "true" && "${LIVE_XREPO}" == "false" && -n "${LIVE_HEAD_OID}" ]]; then

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.

[Suggestion] The discard-path marker release cannot fire on the one discard reason where the target is still valid and the re-emit promise applies — a failed live-metadata fetch. That failure leaves both guard inputs unknowable: on PR_LIVE == '{}', has("isCrossRepository") is false so LIVE_XREPO defaults to true, and .headRefOid // "" yields empty LIVE_HEAD_OID — the guard fails on both inputs, and the checkout-path release is unreachable because this discard exits first. Every scan inside the TTL then sees the fresh PENDING marker and skips the PR as busy. The block's own comment exists precisely to prevent this shape ("a same-repo head would keep being skipped as dispatch-pending for the full TTL with no leg coming — against the re-emit promise above"). — Failure scenario: same-repo PR stamped PENDING; the leg's recheck gh pr view fails transiently (rate limit / 5xx) → INELIGIBLE='metadata fetch failed (API error) — fail-closed' discard → release guard no-ops → the PR is skipped as dispatch-pending for up to the full 30-minute TTL plus a tick instead of being re-emitted on the next tick (self-heals at the TTL).

Witness (verbatim replay of this recheck block with a recording gh stub): failed-fetch arm → discarded (stale=true): true, release writes: ""; control arm (fetch OK, PR closed meanwhile) → release writes: "…statuses/deadbeefcafe -X POST -f state=success -f context=qwen-autofix/dispatch-pending". Enumerating the ladder: fetch-failure is the only discard reason whose release cannot fire — every other same-repo discard yields known LIVE_XREPO=false and a non-empty LIVE_HEAD_OID.

Suggested fix — carry the dispatch-time head sha into the target record and release the known-stamped sha instead of depending on the live fetch that just failed:

# scan side: add the sha already in hand to the target row
#   '. + [{..., head_oid: $head}]'   (LIVE_HEAD is live at selection)
# leg side, on the discard path:
DISCARD_OID="${LIVE_HEAD_OID:-${TARGET_HEAD_OID}}"
if [[ "${DRY_RUN}" != "true" && ( "${LIVE_XREPO}" == "false" || -n "${TARGET_HEAD_OID}" ) && -n "${DISCARD_OID}" ]]; then
  # release on the dispatch-time sha when the live fetch failed

or state in the block's comment that a metadata-failure discard deliberately accepts the TTL-bounded delay.

中文说明

【建议】 丢弃路径的标记释放在唯一一个"target 仍然有效、re-emit 承诺适用"的丢弃原因上无法触发——即 live 元数据获取失败。该失败让守卫的两个输入都不可知:PR_LIVE == '{}'has("isCrossRepository") 为 false,LIVE_XREPO 默认为 true.headRefOid // "" 得到空的 LIVE_HEAD_OID——守卫在两个输入上都不成立,而 checkout 路径的释放不可达,因为此丢弃分支先退出。TTL 之内的每轮 scan 都会看到新鲜的 PENDING 标记并把该 PR 当作 busy 跳过。本块自己的注释正是为了防止这个形态("同仓库 head 会在整个 TTL 内因 dispatch-pending 持续被跳过而没有 leg 到来——违背上面的 re-emit 承诺")。失败场景:同仓库 PR 被打上 PENDING;leg 的 recheck gh pr view 瞬时失败(限流/5xx)→ INELIGIBLE='metadata fetch failed (API error) — fail-closed' 丢弃 → 释放守卫空转 → 该 PR 被跳过最长 30 分钟 TTL 加一个 tick,而不是下一个 tick 重新派发(TTL 到期自愈)。

证据(用带记录功能的 gh stub 对本 recheck 块做逐字回放):获取失败臂 → discarded (stale=true): truerelease writes: "";对照臂(获取成功、期间 PR 被关闭)→ release writes: "…statuses/deadbeefcafe -X POST -f state=success -f context=qwen-autofix/dispatch-pending"。枚举整个 INELIGIBLE 阶梯:获取失败是唯一一个释放无法触发的丢弃原因——其他同仓库丢弃都得到已知的 LIVE_XREPO=false 与非空 LIVE_HEAD_OID

建议修复:把派发时已知的 head sha 带入 target 记录,在 live 获取失败时释放这个已知被打过标记的 sha(而不是依赖刚刚失败的 live 获取);或在本块注释中声明"元数据失败的丢弃有意接受 TTL 有界的延迟"。

— qwen3.8-max via Qwen Code /review (v0.21.13)

// the same way).
const stampSites =
workflow.match(
/gh api "repos\/\$\{REPO\}\/statuses\/[^"]+" -X POST \\\n\s*-f state="\w+" -f context="\$\{DISPATCH_STATUS_CONTEXT\}"/g,

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.

[Suggestion] The marker lifecycle test pins cross-site identity (all three writers + both readers bind ${DISPATCH_STATUS_CONTEXT}) but not per-site semantics: the pin regex -f state="\w+" accepts any state, statuses\/[^"]+ accepts any sha variable, and the skip branch's continue is asserted nowhere. Three one-line workflow mutations each kill the PR's central protection yet survive the entire suite — while the discard-path release IS behaviorally pinned (writes.log assertions); the stamp writer and checkout release are shape-only. — Failure scenario: (a) stamp flips -f state="pending""success" — real markers never match select(.state == "PENDING"), so the dispatch→materialization window reopens: suite passes 177/177. (b) stamp targets a wrong sha variable — the marker never lands in the head's rollup: 392/392 pass across all six workflow test files. (c) the continue is dropped from the marker-skip branch — a busy PR still gets dispatched: 392/392 pass. Comparator proven alive: a sentinel DISPATCH_STATUS_TTL_MINUTES 30→31 mutation fails the lifecycle test.

Suggested fix — pin the per-site semantics:

// the scan stamp must be state=pending on statuses/${LIVE_HEAD}:
expect(reviewScanJob).toContain(
  'gh api "repos/${REPO}/statuses/${LIVE_HEAD}" -X POST \\\n                -f state="pending" -f context="${DISPATCH_STATUS_CONTEXT}"',
);
// both release sites state=success (add the checkout one the
// discard replay already proves), and pin the skip branch body
// (or replay the stamp line with a recording gh stub, exactly
// as the discard release is replayed)
中文说明

【建议】 标记生命周期测试固定的是跨点同一性(三处写入与两处读取都绑定 ${DISPATCH_STATUS_CONTEXT}),但没有固定每处的语义:固定用的正则 -f state="\w+" 接受任何 state,statuses\/[^"]+ 接受任何 sha 变量,跳过分支里的 continue 没有任何断言。三处单行工作流变异每一个都会摧毁本 PR 的核心保护,却都能通过整个测试套件——而丢弃路径的释放是有行为级固定的(writes.log 断言);stamp 写入与 checkout 释放只有形状级固定。失败场景:(a) stamp 翻转为 -f state="success" —— 真实标记永远匹配不上 select(.state == "PENDING"),派发→物化窗口重新打开:套件 177/177 通过。(b) stamp 指向错误的 sha 变量——标记永远不会落在 head 的 rollup 里:全部六个 workflow 测试文件 392/392 通过。(c) 从标记跳过分支删掉 continue —— busy 的 PR 仍会被派发:392/392 通过。比较器活性已证明:哨兵变异 DISPATCH_STATUS_TTL_MINUTES 30→31 会让生命周期测试失败。

建议修复:固定每处语义——scan 的 stamp 必须是 statuses/${LIVE_HEAD}state=pending;两处释放都是 state=success(把丢弃回放已经证明的 checkout 释放也加上);并固定跳过分支的主体(或像丢弃释放那样,用带记录功能的 gh stub 回放 stamp 行)。

— qwen3.8-max via Qwen Code /review (v0.21.13)

@wenshao

wenshao commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round: no action taken

This round found no actionable feedback:

  • No new maintainer feedback and no Critical findings in the actionable sections.
  • No failed checks and no still-red checks.

Critical-only mode is active because the PR's diff grew past this counting window's growth budget (test lines over budget; this is a growth-only engagement). The deferred non-Critical automated-reviewer items listed in the feedback are an audit record for human follow-up — per the workflow rules, they are not modified, resolved, or replied to in this round. Posting @qwen-code /retry would start a fresh counting window if a maintainer wants to re-arm suggestion handling.

No code changes were made and no commit was created.

中文说明

Autofix 审查轮次:未采取任何操作

本轮没有可处理的反馈

  • 可处理区域中没有新的维护者反馈,也没有 Critical 级别的发现。
  • 没有失败的检查,也没有持续失败的检查。

由于本 PR 的 diff 增长超出了当前计数窗口的增长预算(测试行数超出预算;本次为纯增长触发),已进入仅处理 Critical 的模式。反馈中列出的被延迟的非 Critical 自动审查条目是供人工跟进的审计记录——按照工作流规则,本轮不会修改、关闭或回复这些条目。如果维护者希望重新启用建议处理,评论 @qwen-code /retry 即可开启新的计数窗口。

本轮未做任何代码改动,也未创建提交。

Deferred non-Critical feedback

Critical-only mode is active: the PR's diff grew src 39 / test 432 net lines beyond this counting window's baseline (budgets: 400/400). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback continues to flow unaffected during a growth-only engagement (the per-author batch budget applies only after 5 change-producing rounds). (@qwen-code /retry starts a fresh counting window.)

中文说明

已进入仅处理 Critical 的模式:本计数窗口内 diff 净增长已达 源码 39 / 测试 432 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。纯增长触发期间维护者反馈照常流动(按作者的批次预算仅在完成 5 个产生改动的轮次后生效)。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@doudouOUC doudouOUC 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✅

@wenshao

wenshao commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 117 passed · 0 failed · 117 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:117 通过 · 0 失败 · 117 总计

Verification report

Verification report — PR #9329 fix(ci): make autofix busy detection fail closed and mark dispatched PRs

Verdict: merge-ready — 117/117 scripted assertions passed (0 unexpected failures), verified head 9dbaff587269439c6d5ffcf7409af3d01033fe6b (merge-ref checkout HEAD^2; base tip d9d210e).

中文摘要
  • 结论:merge-ready。117 项脚本化断言全部通过,0 项意外失败。
  • A/B 结论:fail-closed 枚举在 base(fail-open)与 head 之间干净翻转——run list 失败、jobs view 失败、双 status 查询部分失败三类故障下,base 保留候选并静默漏检 busy leg(即 Qwen Autofix: review-event storms and duplicate address dispatch waste runner capacity #9296 实测事故形态),head 清空候选、输出 enum_failed=true、fleet 行与 ::warning:: 携带底层错误尾;健康枚举两侧逐位等价(busy 集合、候选保留、无告警)。dispatch-pending 标记的读端(fresh PENDING→skip、stale/SUCCESS/外域/空 rollup/缺 rollup/startedAt 为 null→放行、cutoff 严格 > 边界)与三个写端(same-repo + 非 dry-run 才写、fork/dry-run 不写、stamp 失败降级为告警)全部按设计回放;HAS_PENDING_CHECKS 豁免在 base 门上复现了 stranded marker 阻塞(330 分钟 horizon),head 门放行 stranded marker 且不影响真实在飞检查。
  • 新契约测试非空转:8 个突变体中 7 个被目标测试杀死(含两个只改行为不碰 pin 字符串的突变体,证明行为回放层独立生效);唯一存活者是 stamp 的 description 文案(无害覆盖缺口,非缺陷)。
  • Findings(均非阻塞):① 显式 workflow_dispatch 豁免保留候选时,warning/fleet 行仍写 "no scan targets dispatched this pass / scan dispatch skipped this pass",与豁免语义矛盾(仅日志措辞);② description 文案无测试 pin。
  • 未覆盖:per-commit 归因(depth-2,3 个提交本地仅可达 1 个;commit 1 的路由改动已在 base tip,净 diff 只含 commit 2+3);真实双 scan/orphan marker 的端到端(沙箱无 GitHub token,仅以回放复现判定谓词形态);yamllint(容器无 pip3,以 actionlint 1.7.12 exit 0 + 严格 YAML 解析替代);vitest 有一次既有的 worker RPC 超时(base A/A 同样复现,非本 PR 引入)。

Central claim and A/B

Central claim: any busy-enumeration failure empties the scan's candidate set (fail-closed) unless the dispatch is an explicit workflow_dispatch, and the qwen-autofix/dispatch-pending commit-status marker closes the dispatch→leg-materialization window without blocking past its 30-minute TTL.

Both harnesses carve the changed blocks verbatim out of the parsed YAML (yaml package) and replay them under bash --noprofile --norc with a PATH-stubbed gh that applies --jq via real jq — the scan step has no set line, and the replay honors that. Witness: evidence/01-enum-ab-base-vs-head.png, evidence/02-marker-lifecycle-ab.png.

cell base (control) head result
healthy enum, one busy leg candidates kept, busy=101 identical equivalence ✓
healthy enum, no live runs candidates kept, busy empty identical equivalence ✓
run-list failure candidates KEPT, busy silently empty (fail-open) candidates EMPTIED, enum_failed=true, fleet fail-closed, error tail in warning flip ✓
jobs-view failure (measured incident shape) busy legs invisible, would re-dispatch over live legs candidates emptied, enum_failed=true, broke after failing view flip ✓
list failure + explicit workflow_dispatch kept kept (carve-out), enum_failed still signalled carve-out ✓
list failure + FORCED_PR from trusted pull_request_review kept emptied (carve-out narrowed to explicit dispatches) narrowing ✓
first status query fails, second healthy degrades to partial view fails closed on first failure (stated tradeoff) tradeoff ✓

Marker mechanism (base has zero references to the marker; all of it is new):

surface cells result
scan-side skip predicate fresh PENDING→skip; stale→pass; exactly-at-cutoff→pass (strict >); +1s→skip; SUCCESS→pass; foreign context→pass; empty rollup→pass; missing rollup key→pass; CheckRun-typed entry→pass; null startedAt→pass; month-boundary compares 12/12 ✓
HAS_PENDING_CHECKS exemption (A/B) base gate: stranded marker alone blocks (the ~330-min TTL bug); head gate: stranded marker blocks nobody, genuine in-flight check still blocks (alone and beside marker), stuck >330m aged out, foreign PENDING context still blocks, Qwen-Autofix review-address leg still blocks, nonblocking-listed check doesn't 8/8 ✓
dispatch-time stamp same-repo non-dry-run stamps once on the live head with state=pending + marker context; fork→none; dry-run→none; failed stamp→warning, exit 0 8/8 ✓
checkout-path release same-repo stamps success on checked-out head; fork→none; dry-run→none; unset HEAD_REPO falls back to REPO 5/5 ✓
discard-path release same-repo discard releases on live head; fork→none; missing sha→none; dry-run→none 5/5 ✓

Wiring (19/19): enum_failed output exposed and consumed inside the schedule clause of both the issue-phase if: and its concurrency group (non-schedule events bypass it); DRY_RUN wired into the prepare-branch step; exactly 3 status-POST sites, all binding ${DISPATCH_STATUS_CONTEXT}, read by the same variable twice; base PR_META fetch already included statusCheckRollup (the check side costs zero extra API calls, as claimed).

Reviewer Test Plan walkthrough

  1. Fail-closed reads — the plan says API failures "cannot be forced on demand"; the stub-gh harness forced them anyway (cells above). Reads as claimed. ✓
  2. Second scan within 30 min skips — predicate replayed over fixture rollups (fresh PENDING → skip, exact log line ⏳ #${PR}: dispatch pending (marker fresher than …) — skipping present at scan step line 3068). Shape reproduced by replay; the live double-scan trigger (real dispatch + build-cli window) is not reproduced end-to-end — no GitHub credential in this sandbox.
  3. Orphan marker expires by age — stale PENDING (>30m) → dispatchable, exactly-at-cutoff boundary probed. Same replay caveat.
  4. Fork PRs unstamped — all three write sites refuse fork heads and dry runs in replay. ✓

Findings (non-blocking)

F1 (nit) — fail-closed warning overstates in the explicit-dispatch carve-out. In the fail-closed branch the ::warning::… no scan targets dispatched this pass and the fleet row "scan dispatch skipped this pass" are emitted before the carve-out if, so an explicit workflow_dispatch that keeps its candidates (A/B cell 5) logs "no targets dispatched" while targets are in fact dispatched. Log wording only; the enum_failed signal and the carve-out behavior are correct. Evidence: logs/enum-ab.txt cell 5 (warning present, candidates kept).

F2 (completeness) — the stamp description= strings are the one unpinned axis. Mutation M7 (description text changed) survives the whole suite by design of the pins. Harmless prose, not a contract; noted for completeness, not as a merge condition.

Mutation matrix (new contract tests)

Witness: evidence/03-mutation-matrix.png (live run), logs/mutation-matrix.txt.

mutant expected observed caught by
M1 revert enum block to base RED RED enum test (block absent)
M2 carve-out widened to review events RED RED enum test (string pin)
M3 drop enum_failed echo RED RED enum test, behavioral (expected '' to contain 'enum_failed=true')
M4 freshness >< RED RED lifecycle test
M5 drop HAS_PENDING_CHECKS exemption RED RED lifecycle test
M6 drop discard-path release RED RED both marker tests (write-log + stamp-site census 3→2)
M7 description text changed SURVIVE SURVIVE — (F2)
M8 discard release state=successpending (all pinned strings intact) RED RED release test, behavioral (expected … to contain 'state=success')

No mutant regressed a killed test to survived; the two behavioral-layer kills (M3, M8) prove the replay assertions fire independently of the string pins. Every mutant run restored the tree (git status clean after each).

Targeted gates

  • Contract suite at head: 179/179 pass (evidence/04-contract-suite-green.png). One vitest worker onTaskUpdate RPC timeout reported as an unhandled error — reproduced identically on the base suite (176/176 + same error, A/A control), so pre-existing/environmental on this loaded runner, not the PR.
  • Base suite at base (gate-live control): 176/176 pass.
  • actionlint 1.7.12 (repo-pinned, sha-verified install): exit 0 on all workflows.
  • shellcheck 0.11.0 at --severity=error on the two changed run blocks (extracted verbatim, # shellcheck shell=bash directive added): 0 findings; 2 pre-existing style warnings (export X="$(mktemp …)" SC2155) also present at base.
  • bash -n on both extracted run blocks: clean.

Not covered

  • Per-commit attribution: depth-2 checkout reaches only 1 of the 3 commits in the metadata; commit 1's route-gate clause (pull_request_review on closed PRs) is already in the base tip, so the aggregate diff is commits 2+3. Verified the aggregate only.
  • Live end-to-end of the double-scan window and orphan-marker expiry (plan steps 2–3): requires a GitHub PAT + real dispatches; this sandbox has none. The replays reproduce the decision predicates and log shapes, not the trigger.
  • yamllint 1.35.1: container has no pip3/pip module; could not install. Substituted actionlint (which parses the workflow YAML) plus strict yaml parsing of both files in every harness.
  • Repo-wide gates (ESLint, prettier, full test suite) not re-run — CI covers them; this round scoped to the changed surface.
  • The repo's own --shellcheck gate lints standalone .sh files only and runs actionlint with -shellcheck=; embedded workflow bash is not shellchecked by any repo gate, so the error-severity run above is mine, not the repo's.

Methodology

Environment: node:22-bookworm CI verify container, merge-ref checkout at depth 2, npm ci + build pre-done. All harnesses (enum-ab.mjs, marker.mjs, wiring.mjs, mutate.mjs, matrix-assert.mjs, extract-steps.mjs) live in this artifact dir; raw logs in logs/. Blocks were carved from yaml.parse()d workflow text (never retyped) and replayed under bash --noprofile --norc with PATH-stubbed gh/recording stubs; mutations were applied to a scratch copy of the working-tree YAML and restored+verified after each. Assertion counts: enum-ab 40, marker 41, wiring 19, matrix-assert 8, gates 9 (3 new tests green, full suite green, base A/A suite green, actionlint, shellcheck-error, bash -n ×2) = 117 pass / 0 fail.

Evidence images

01-enum-ab-base-vs-head

02-marker-lifecycle-ab

03-mutation-matrix

04-contract-suite-green

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@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. ✅

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round: no action taken

This round found no actionable feedback on PR #9329 (head 9dbaff58):

  • No new maintainer feedback, no reviews, no inline comments, and no Critical findings in the actionable sections (only feedback newer than the last evaluation at 2026-08-17T14:34:10Z is considered).
  • No failed checks and no still-red checks; all recorded check conclusions are SUCCESS or SKIPPED.
  • No conflict with main, so no base merge was performed.

Critical-only mode is active because the PR's diff grew past this counting window's growth budget (src 39 / test 432 net lines vs 400/400 budgets; test lines over budget — a growth-only engagement). The one item in the deferred non-Critical section is the bot's own triage verification-in-progress comment, an audit record for human follow-up — per the workflow rules it is not modified, resolved, or replied to in this round. A maintainer can post @qwen-code /retry to start a fresh counting window and re-arm suggestion handling.

No code changes were made and no commit was created. The branch stays at 9dbaff58.

中文说明

Autofix 审查轮次:未采取任何操作

本轮在 PR #9329(head 9dbaff58)上没有发现可处理的反馈

  • 可处理区域中没有新的维护者反馈、没有 review、没有行内评论,也没有 Critical 级别的发现(仅考虑 2026-08-17T14:34:10Z 上次评估之后的新反馈)。
  • 没有失败的检查,也没有持续失败的检查;记录中的所有检查结论均为 SUCCESS 或 SKIPPED。
  • main 无冲突,因此未执行基分支合并。

由于本 PR 的 diff 增长超出了当前计数窗口的增长预算(源码 39 / 测试 432 净行,预算 400/400;测试行数超出预算——本次为纯增长触发),已进入仅处理 Critical 的模式。被延迟的非 Critical 区域中唯一的条目是 bot 自身的 triage 沙箱验证进行中的评论,属于供人工跟进的审计记录——按照工作流规则,本轮不会修改、关闭或回复该条目。维护者可以评论 @qwen-code /retry 开启新的计数窗口,重新启用建议处理。

本轮未做任何代码改动,也未创建提交。分支保持在 9dbaff58

Deferred non-Critical feedback

Critical-only mode is active: the PR's diff grew src 39 / test 432 net lines beyond this counting window's baseline (budgets: 400/400). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback continues to flow unaffected during a growth-only engagement (the per-author batch budget applies only after 5 change-producing rounds). (@qwen-code /retry starts a fresh counting window.)

中文说明

已进入仅处理 Critical 的模式:本计数窗口内 diff 净增长已达 源码 39 / 测试 432 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。纯增长触发期间维护者反馈照常流动(按作者的批次预算仅在完成 5 个产生改动的轮次后生效)。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@wenshao
wenshao added this pull request to the merge queue Aug 18, 2026
Merged via the queue into QwenLM:main with commit a4a3850 Aug 18, 2026
168 of 169 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.14.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants