Skip to content

ci: enrich deferred-findings tracking issues with PR context and assignment - #11080

Merged
wenshao merged 10 commits into
mainfrom
ci/deferred-findings-issue-context
Sep 7, 2026
Merged

ci: enrich deferred-findings tracking issues with PR context and assignment#11080
wenshao merged 10 commits into
mainfrom
ci/deferred-findings-issue-context

Conversation

@yiliang114

@yiliang114 yiliang114 commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

The autofix loop files one tracking issue per PR for verified review findings whose fixes lie outside that PR's footprint (upsert-deferred-issue.sh). Before this change the issue carried a constant title (Deferred review findings from PR #N), a bare boilerplate body, and no assignee, so nobody could tell at a glance what the issue was about or who should follow up. After this change the create path fetches the PR once and makes the issue self-describing:

  • The title carries the PR title: Deferred review findings from PR #10991: refactor(daemon): decouple extension activation refresh.
  • The body names the PR context — number and author — and cc's the author, so the person whose PR produced the findings is notified even when GitHub refuses the assignment (external contributors are not assignable; the mention is what actually reaches them). The PR title rides on the issue TITLE only: GitHub stores and renders that surface as plain text, while the body is markdown, and a fully contributor-controlled title has no escape chain that closes it — [URGENT](https://evil.example/phish) would render as a live attacker-chosen link inside bot-authored text, and an unclosed <details> would fold the findings list away from the raw-body dedupe corpus. The title copy is therefore flattened and capped for the title and never copied into the body; .github/scripts/upsert-deferred-issue.sh records the rule as Do NOT re-add it; if a title ever has to appear in the body, put it in a code span, never in prose.
  • The issue is assigned to the PR author by a separate best-effort call after creation. The create is never retried: POST /repos/{owner}/{repo}/issues is not idempotent, and the failures that would reach a retry are the ambiguous ones (connection reset, gateway 502, a read timeout after the server already committed), so re-POSTing can mint a second tracking issue carrying the same marker that the next round's newest-first lookup orphans forever. A rejected assignment only warns — the body's cc @author mention already reaches them.
  • Every rc: bullet deep-links to its original review comment (…/pull/N#discussion_r<id>), so the bare internal comment id is no longer the only handle.

Everything is best-effort: a PR-context fetch that fails or returns nothing usable degrades to the previous bare title, no assignee and no cc, and warns — metadata must never lose findings. The append path is unchanged and makes no pulls/ call at all (the context fetch is creation-only).

Why it's needed

Over the past week this mechanism created ~100 tracking issues (e.g. #11074), and each one reads as scattered noise: a title that says nothing about the content, a body of - rc:<internal id> bullets with no way back to the original review comment, and no assignee. A maintainer has to open the source PR and hunt through its review threads to figure out what a tracking issue even refers to, and nobody is pointed at for follow-up. This PR puts the context and the owner on the issue itself.

Reviewer Test Plan

How to verify

The recording-gh test suite pins the behavior end to end; run the upsert block (pass --config, as npm run test:scripts does: the bare form resolves the root config's scripts project, which has no config file of its own and so falls back to vitest's 5s default testTimeout — this case measures ~4.8s and times out):

npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js -t 'upserts deferred findings into a per-PR issue that survives the merge'

New cases cover: enriched title + assignee + deep link on the create call; degraded create (failed PR fetch → bare title, no assignee, still persisted, plus a could not fetch PR #5 context warning naming the captured reason); degraded create when the fetch exits 0 with an unusable body (200 text/html → same degradation, warning says the call returned no usable PR object); assignment rejected (separate assign call fails → could not assign warning, exactly one create POST, findings still persisted); bot-authored PR (never self-assigned, no cc); malformed .user.login shapes (a space, an @, 40 characters) never reaching the cc @… mention or the assignees[] argument; enriched-title adoption by the lookup with the colon guard (PR #5's base never prefix-adopts PR #50's issue); the PR title reaching the plain-text issue TITLE raw (flatten + 80-codepoint cap only: [URGENT](https://evil.example/y) <details> fix @foo and ping &#64;admin &commat;x + 100 As both come back verbatim apart from the newline fold and the cap) and reaching the markdown-rendered body in no spelling — evil.example, URGENT, <details>, fix @foo and the ZWSP-neutralized @foo are all asserted absent from -f body= while from PR #5 by someone stays; expect(scriptEscapeSites).toHaveLength(1) keeps the retired title-neutralization site retired, so re-adding one has to be deliberate; the ready-for-agent pointer in the body attaching to the per-item issue rather than to this tracking issue (which now carries an assignee and is therefore filtered out of the scheduled no:assignee scan); and the append path issuing no pulls/ call.

The lookup's title fallback also keeps adopting existing bare-form issues (exact match) — pinned by the pre-existing marker-stripped case.

Evidence (Before & After)

Before (live issue #11074):

Title: Deferred review findings from PR #10991
Body:  <!-- autofix-deferred pr=10991 -->
       Verified review findings from PR #10991 whose fixes lie outside that PR's footprint, …
       - rc:3937553394 `packages/sdk-typescript/src/daemon/events.ts`: Verified real: every activation route …

After (local dry-run of the script against a recording gh stub):

Title: Deferred review findings from PR #5: Some PR title
Body:  <!-- autofix-deferred pr=5 -->
       Verified review findings from PR #5 by someone whose fixes lie outside that PR's footprint, deferred by the autofix loop for follow-up. cc @someone. Each rc: item links back to its original review comment. A maintainer or the PR author can turn any item into its own issue/PR and apply the ready-for-agent flow to that issue — nothing here is scheduled automatically. …
       - rc:7 `src/a.ts`: real — [comment](https://github.com/o/r/pull/5#discussion_r7)
       (then assigned by a separate POST repos/o/r/issues/77/assignees -f assignees[]=someone)

Tested on

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

Environment (optional)

Unit-test level only: the script runs against a recording gh stub under vitest (script is bash + jq + gh, same as before — no new runtime dependencies).

Risk & Scope

  • Main risk or tradeoff: one extra API call (GET pulls/N) on the create branch only, best-effort with degradation to the old form. Assignment can be rejected for external authors — handled by the separate best-effort assign call (warning only) plus the cc mention, so the author is still reached.
  • Not validated / out of scope: the ~100 existing tracking issues keep their old title/body (the lookup still adopts them by marker/exact title; only new issues get the enriched form). rv:/ic: bullets deliberately get no link — their cross-round identity IS the rendered line, and a suffix would re-publish every already-persisted rv/ic item once across existing issues.
  • Breaking changes / migration notes: none; dedupe anchors are extended, not replaced (bare title and marker-stripped adoption both keep working).

Linked Issues

Reference: #11074 is the motivating example of the current form.

中文说明

这个 PR 做了什么

autofix 循环会为每个 PR 建一个 tracking issue,记录那些"已验证属实、但修复超出该 PR 改动范围"的 review 发现(upsert-deferred-issue.sh)。改动前这个 issue 只有固定标题(Deferred review findings from PR #N)、一段裸模板正文、没有 assignee,一眼看不出说的是什么、该谁跟进。改动后创建路径会拉一次 PR 元数据,让 issue 自描述:

  • 标题带上 PR 标题:Deferred review findings from PR #10991: refactor(daemon): decouple extension activation refresh
  • 正文写明 PR 上下文(编号、作者),并在正文里 cc 作者——即使 GitHub 拒绝 assign(外部贡献者不可被 assign),提及也能让 PR 作者收到通知。PR 标题只出现在 issue 标题上:GitHub 把标题当纯文本存储与渲染,而正文是 markdown,完全由贡献者控制的标题没有任何转义链能封住——[URGENT](https://evil.example/phish) 会在 bot 署名的文本里渲染成可点击、由攻击者选定目标的链接,未闭合的 <details> 会把 findings 列表折叠起来、从原始正文的去重语料里消失。因此标题只做 flatten 与截断后进入 issue 标题,绝不副本进正文;.github/scripts/upsert-deferred-issue.sh 把这条规则写成了 Do NOT re-add it; if a title ever has to appear in the body, put it in a code span, never in prose
  • 建完 issue 后用独立的尽力调用把 PR 作者设为 assignee。创建绝不重试:POST /repos/{owner}/{repo}/issues 不幂等,而能走到重试的恰恰是模糊失败(连接重置、网关 502、服务端已提交后的读超时),重新 POST 可能铸出第二个带相同 marker 的 tracking issue,被下一轮的 newest-first lookup 永久孤儿化。assignment 被拒只告警——正文的 cc @author 提及已经能触达作者。
  • 每条 rc: 附原评论深链(…/pull/N#discussion_r<id>),内部评论 id 不再是唯一线索。

全部 best-effort:PR 元数据拉取失败或返回内容不可用时降级回旧的裸标题、无 assignee、无 cc,并发出告警——元数据问题绝不能丢 findings。追加(append)路径完全不变,且根本不发 pulls/ 请求(上下文拉取仅限创建路径)。

为什么需要

过去一周该机制建了约 100 个 tracking issue(例如 #11074),每个都显得散乱:标题看不出内容、正文是一串没有回链的 - rc:<内部 id>、没有 assignee。maintainer 必须打开源 PR 翻 review 线程才能搞清 issue 指的是什么,也没有人被指向去跟进。这个 PR 把上下文和责任人直接放到 issue 上。

评审验证计划

如何验证

recording-gh 测试套件端到端 pin 住了行为,跑 upsert 测试块(要带 --config,与 npm run test:scripts 一致:不带时命中的是根配置的 scripts project,而它自己没有配置文件,于是回退到 vitest 默认的 5s testTimeout——这个用例实测 ~4.8s,会超时):

npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js -t 'upserts deferred findings into a per-PR issue that survives the merge'

新增用例覆盖:创建调用的增强标题 + assignee + 深链;降级创建(PR 拉取失败 → 裸标题、无 assignee、仍落盘,并输出 could not fetch PR #5 context 告警并带上捕获到的原因);拉取退出码 0 但正文不可用的降级创建(200 text/html → 同样降级,告警改为说明该调用没有返回可用的 PR 对象);assignment 被拒(独立的 assign 调用失败 → could not assign 告警、恰好一次创建 POST、findings 仍落盘);bot 自己的 PR(永不自我 assign、不 cc);畸形 .user.login(含空格、含 @、40 字符)绝不进入 cc @… 提及与 assignees[] 参数;lookup 对增强标题的收养及冒号守卫(#5 的基础标题不会前缀误吞 #50 的 issue);PR 标题以原样(仅 flatten 与 80 码点截断:[URGENT](https://evil.example/y) <details> fix @fooping &#64;admin &commat;x + 100 个 A 除换行折叠与截断外均原样返回)进入纯文本的 issue 标题,且以任何拼写都不进入 markdown 渲染的正文——evil.exampleURGENT<details>fix @foo 以及 ZWSP 净化后的 @foo 均被断言不出现在 -f body= 中,同时 from PR #5 by someone 保留;expect(scriptEscapeSites).toHaveLength(1) 把已退掉的标题净化点钉住,重新加回必须是有意为之;正文里的 ready-for-agent 指引挂在单条 finding 自建的那个 issue 上,而不是这个 tracking issue(它现在带 assignee,因而被定时扫描的 no:assignee 过滤掉);以及 append 路径pulls/ 请求。

lookup 的标题兜底仍会收养存量的裸格式 issue(精确匹配)——由既有的 marker-stripped 用例 pin 住。

前后证据

改动前(线上 issue #11074):

Title: Deferred review findings from PR #10991
Body:  <!-- autofix-deferred pr=10991 -->
       Verified review findings from PR #10991 whose fixes lie outside that PR's footprint, …
       - rc:3937553394 `packages/sdk-typescript/src/daemon/events.ts`: Verified real: every activation route …

改动后(本地用 recording gh stub 跑脚本的 dry-run 输出):

Title: Deferred review findings from PR #5: Some PR title
Body:  <!-- autofix-deferred pr=5 -->
       Verified review findings from PR #5 by someone whose fixes lie outside that PR's footprint, deferred by the autofix loop for follow-up. cc @someone. Each rc: item links back to its original review comment. A maintainer or the PR author can turn any item into its own issue/PR and apply the ready-for-agent flow to that issue — nothing here is scheduled automatically. …
       - rc:7 `src/a.ts`: real — [comment](https://github.com/o/r/pull/5#discussion_r7)
       (随后由独立调用 POST repos/o/r/issues/77/assignees -f assignees[]=someone 完成指派)

测试环境

OS 状态
🍏 macOS
🪟 Windows ⚠️ 仅 CI
🐧 Linux ⚠️ 仅 CI

运行环境(可选)

仅单测层面:脚本在 vitest 下对 recording gh stub 运行(bash + jq + gh,与之前一致,无新运行时依赖)。

风险与范围

  • 主要风险/权衡:仅创建分支多一次 API 调用(GET pulls/N),best-effort、失败降级回旧形态。外部作者可能无法被 assign——由"独立的尽力 assign 调用(失败只告警)+ 正文 cc"兜底,作者仍能收到通知。
  • 未验证/不在范围内:存量约 100 个 tracking issue 保持旧标题/正文(lookup 仍通过 marker/精确标题收养它们;只有新 issue 是增强形态)。rv:/ic: 条目刻意不加链接——它们的跨轮身份就是渲染行本身,加后缀会把所有已落盘的 rv/ic 条目一次性重新发布,在存量 issue 上造成重复。
  • 破坏性变更/迁移说明:无;去重锚点是扩展而非替换(裸标题和 marker 被删后的收养都继续有效)。

关联 Issue

参考:#11074 是当前形态的动机示例。

…gnment

The autofix loop's deferred-findings upsert files one tracking issue per
PR for verified review findings whose fixes lie outside that PR's
footprint. The issue it created carried a constant title, a bare
boilerplate body, and no assignee, so a maintainer looking at the issue
could not tell what it was about or who should follow up — over the past
week that produced ~100 issues that read as scattered noise.

The create path now fetches the PR once and makes the issue
self-describing: the title carries the PR title, the body names the PR
context (number, title, author), cc's the author, and every rc bullet
deep-links to its original review comment. The issue is assigned to the
PR author at creation; external contributors are not assignable, so the
create retries once without the assignment rather than losing the
findings, and the cc in the body is what actually reaches them. All of
it is best-effort: a failed PR fetch degrades to the previous bare
title and no assignee — metadata must never lose findings.

The title is also the lookup's fallback anchor when a maintainer edit
drops the body marker. It now accepts both the bare form and the
enriched "base: <PR title>" form, guarded by the colon so PR #5's base
never prefix-matches PR #50's issue. The rc link suffix is safe on
identity because rc dedup is id-anchored; rv/ic bullets deliberately get
no link — their cross-round identity IS the rendered line, and a suffix
would render every already-persisted rv/ic item as new, a one-time
duplicate wave over the existing tracking issues.

The PR title is API-derived content published under the bot identity,
so it receives the same mention/comment-opener neutralization as the
reason rendering (canonical spelling, which moves the script's escape
census pin from 1 to 2), and its cap slice happens in jq so a CJK title
cannot be byte-cut under a C locale.

Tests: the recording gh stub learns the pulls endpoint and an
assignment-failure injection; new cases cover the degraded create, bot
authors, the assign fallback, enriched-title adoption, the number-prefix
collision, and title neutralization. The upsert test spawns ~70
subprocesses, so it gets an explicit 30s bound like its spawn-heavy
neighbors instead of flaking on exec-scanning hosts.
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

⚠️ Qwen Triage ended earlyview run. It stopped before finishing; check the run log.

⚠️ Qwen Triage 提前结束 —— 查看运行。未跑完,请查看运行日志。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓ — every required heading is present, including all three Risk & Scope bullets and the Chinese section.

Problem: observed, and I checked it rather than taking the description's word for it. Issue #11074 is live and open right now: authored by qwen-code-dev-bot, zero assignees, titled Deferred review findings from PR #10991, body is the bare boilerplate followed by - rc:3937553394 … — an internal comment id with no way back to the thread it came from. The "Before" block in the description matches the live issue exactly. Not theoretical hardening.

Direction: this is the repo's own autofix tooling rather than shipped product, so there is no user-facing contract to weigh and the CHANGELOG signal does not really apply. Two honest questions, neither a blocker:

  • Who should own these? By definition these are findings whose fix lies outside the PR's footprint — usually pre-existing problems in the surrounding code that a review happened to surface. Assigning and cc'ing the PR author points at the person least obliged to fix them, and for external contributors GitHub rejects the assignment anyway, so what actually reaches them is a notification about work they never touched. The repo does have an ownership mechanism (assign-issue-owner.mjs + issue-owners.json), but it derives the owner purely from labels and these issues carry none, so it cannot fire here. Is "the PR author" the intended default owner, or should these stay unassigned and carry a maintainer-facing label instead?
  • Readability vs. volume. This makes the ~100 tracking issues legible; it does not reduce how many get created, and the description says so plainly. Is a volume lever — a higher defer bar, or a per-PR cap — a separate follow-up?

Size: not applicable — no core paths are touched. 82 production lines (.github/scripts/upsert-deferred-issue.sh) plus 119 test lines (scripts/tests/qwen-autofix-workflow.test.js), 201 total across 2 files.

Approach: the scope feels right, and it lands close to what I would have written independently — fetch the PR once on the create branch only, put the title in the issue title (capped, since GitHub enforces an issue-title length limit and a long CJK title would otherwise 422 the create), name the author in the body, derive the review-comment URL from the id the bullet already carries (no extra call), and extend the lookup's dedupe anchors instead of replacing them so the existing bare-form issues keep getting adopted. I checked the reuse angle rather than assuming it: both call sites — qwen-autofix.yml and autofix-push-and-report.sh — hand this script a clean /usr/bin/env -i child carrying only WORKDIR/PR/REPO/AUTOFIX_BOT/UPSERT_SRC/GITHUB_TOKEN/GH_HOST/RUNNER_TEMP/PATH, so the PR title and author genuinely are not already in hand. Fetching them inside the script is the minimal path, and it keeps fork-controlled text out of that hardened child's env. The env contract is unchanged, so neither caller needs touching. The one drive-by is the 30 s test timeout, which matches six pre-existing spawn-heavy neighbours in the same file — fine to keep here.

Risk: no elevated risk signals — neither changed file matches the high-risk path list.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓ —— 所有必需小标题都在,包括 Risk & Scope 的三个要点和中文说明。

问题:已观测到,而且是我自己去核实的,不是照抄 PR 描述。 issue #11074 现在就在线上、处于 open 状态:作者是 qwen-code-dev-bot,assignee 为空,标题是 Deferred review findings from PR #10991,正文是裸模板加一句 - rc:3937553394 …——只有一个内部评论 id,回不到它来自的 review 线程。PR 描述里的 "Before" 与线上 issue 完全一致。这不是理论性加固。

方向: 这是仓库自己的 autofix 工具链,不是发布出去的产品能力,所以没有用户可见契约要权衡,CHANGELOG 信号在这里也基本不适用。两个坦率的问题,都不是阻塞项:

  • 这些 issue 该归谁? 按定义,这里的 finding 都是"修复超出该 PR footprint"的——多数是 review 顺手发现的、周边代码里本来就存在的问题。把它 assign 并 cc 给 PR 作者,指向的恰恰是最没有义务去修的人;而对外部贡献者 GitHub 本来就会拒绝 assign,真正到达他们的只是一条与自己无关的通知。仓库确实有一套归属机制(assign-issue-owner.mjs + issue-owners.json),但它完全由 label 推导 owner,而这些 issue 没有任何 label,所以在这里无法生效。"PR 作者"是预期的默认 owner 吗?还是应该保持不 assign、改挂一个面向 maintainer 的 label?
  • 可读性 vs 数量。 这个 PR 让约 100 个 tracking issue 变得可读,但没有减少它们的产生数量,描述里也写明了这一点。数量层面的手段(提高 defer 门槛,或按 PR 限流)是单独的后续工作吗?

规模: 不适用——没有触及核心路径。生产代码 82 行(.github/scripts/upsert-deferred-issue.sh),测试 119 行(scripts/tests/qwen-autofix-workflow.test.js),2 个文件共 201 行。

方案: 范围合理,也和我独立想到的做法很接近——只在创建分支拉一次 PR;把 PR 标题放进 issue 标题(做了截断,因为 GitHub 对 issue 标题长度有限制,过长的中文标题会让创建调用 422);在正文写明作者;review 评论链接直接由 bullet 里已有的 id 推导(不额外调用 API);并且是扩展而非替换 lookup 的去重锚点,这样存量的裸格式 issue 仍会被收养。复用这一点我是查过的,不是想当然:两个调用方——qwen-autofix.ymlautofix-push-and-report.sh——都是通过干净的 /usr/bin/env -i 子进程调用本脚本,只传入 WORKDIR/PR/REPO/AUTOFIX_BOT/UPSERT_SRC/GITHUB_TOKEN/GH_HOST/RUNNER_TEMP/PATH,所以 PR 标题和作者确实不在手边。在脚本内部自己拉取是最小路径,同时也把 fork 可控文本挡在那个加固子进程的 env 之外。env 契约没有变化,两个调用方都不需要改。唯一的顺手改动是 30 秒测试超时,与同文件里已有的六个 spawn 密集型测试一致——放在这里合适。

风险: 无升级风险信号——两个改动文件都不匹配高风险路径清单。

进入代码审查 🔍

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

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

I wrote down what I would do before reading the diff, and this is essentially it — so most of my time went into checking whether the claims in the comments actually hold, rather than into the shape of the change. They do.

The load-bearing claim checks out. Appending a deep link to the rc: bullet is only safe if rc dedupe does not read the rendered line, and it does not: the corpus check for review_comment is ($klines | any(test($r.anchor))) against anchor = "^- rc:<id> ", and unique_by keys rc on [.src, .id]. Neither touches .line. rv/ic are the opposite — they key on key = (.line | normkey) and fullkey = (.raw | normkey) — so a suffix there really would render every already-persisted item as new. Leaving them unlinked is a correct reading of the mechanism, not an arbitrary omission. The same reasoning covers backwards compatibility: the bare-form rc lines already sitting in the ~100 live tracking issues still match the anchor, so the append path does not produce a duplicate wave.

The colon guard does what it says. startswith($t + ":") cannot let PR #5's base adopt PR #50's issue (…PR #50 does not start with …PR #5:), and PR #50's lookup cannot adopt PR #5's enriched issue either. The bare exact match survives for pre-existing issues, and the whole lookup is already restricted to creator=${AUTOFIX_BOT}, so a human-titled issue can never be adopted.

Sanitization is at parity with the existing publish site, and the escape-site count test moving 1 → 2 matches reality — there are now exactly two gsub("<!--"; …) sites in the script. Slicing the title inside jq rather than in bash is the right call for the reason the comment gives. PR_AUTHOR is charset-gated to ^[A-Za-z0-9-]{1,39}$ before it reaches the one deliberately un-defused @, so the cc mention cannot carry anything else in.

Nothing else consumes the format. I grepped the title and body strings repo-wide: the only readers are the script and its own test. docs/design/autofix-growth-audit.md describes the pipeline (single upsert, rc-id dedupe, token neutralization) but never the title or body wording, so no doc goes stale here.

One thing worth confirming, since it is easy to get backwards: gh api -f is the raw string field and -F is the typed one, so -f "assignees[]=${PR_AUTHOR}" cannot be coerced to a number even for an all-digit login. That matches the repo's existing split (autofix-push-and-report.sh uses -F pr= precisely because it needs a GraphQL Int). I also confirmed against the real API, with a read-only GET, that gh serializes key[]=value as an array — the recording stub only asserts argv, so it cannot tell you that.

Non-blocking observations

  1. Metadata degradation is silent. When GET pulls/{PR} fails but the create succeeds, gh_err_reset before the create wipes the fetch's stderr and the round logs only 🗂 … new issue #N. Nothing records that this particular issue got the bare title and no assignee. The header contract's "every failure path warns" is about persistence, so this is not a violation — but a one-line warning when PR_JSON comes back empty would make a bare-title issue diagnosable after the fact, which is exactly the confusion this PR exists to remove.
  2. The retry's failure shape is a duplicate issue, not a lost one. For the expected 422 ("Unassignable user") GitHub creates nothing, so the assignee-less retry is clean and the cc still reaches the author. But if a create succeeded server-side and the client still saw a non-zero exit — a transport break, or --jq '.number' failing on a malformed response — the retry opens a second tracking issue for the same PR; the lookup then adopts the newest and orphans the other. Low probability, and "never lose findings" is this script's stated priority, so the tradeoff is defensible. Naming it because duplicate noise is what the PR is fighting.
  3. Small duplication. The PR_AUTHOR non-empty-and-not-bot condition is written twice (cc, then the create branch) and the create call appears three times, differing only by the assignees flag. Fine as-is under the repo's simplicity rule — I would only collapse it if a fourth variant appears.

Testing

This is an unattended CI run, so I did not build or execute anything from this PR — the evidence below is the PR's own CI, read through the API once at the reviewed commit. No polling: this is a snapshot, not a settled result.

Zero checks were red at fetch time. The job that actually pins this change — Test (ubuntu-latest, Node 22.x), which runs scripts/tests/qwen-autofix-workflow.test.js — had not reported yet, and neither had Lint & Static or the integration job, so there is no failing log to quote. The macOS and Windows unit jobs are skipped, which is the right coverage for a bash + jq script; ubuntu is the only lane that exercises it. The skipped verify / tmux-testing / publish-* rows are this workflow's own gated lanes, not something the PR turned off.

Not verified: the unit suite's own result — still in flight at fetch time. The table below is rewritten in place once CI settles, and approval is deferred with it.

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

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

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

Sandboxed verification would settle the one thing the stub cannot: @qwen-code /verify — that a genuinely unassignable author produces a 422 on the first create and the assignee-less retry then persists the findings. In the suite that path is driven by ASSIGN_FAIL exiting 1 on any call whose argv contains assignees, which asserts the recovery branch without GitHub's actual rejection semantics behind it. Everything else the PR claims is pinned directly on recorded argv (title string, assignees[]=someone, the cc mention, the discussion_r URL, both degradation paths, the #5-vs-#50 collision case), which is a strong oracle for a script whose whole job is building those arguments — and the array-syntax question I already answered myself above. No TUI surface here, so /tmux does not apply. Real-scenario terminal testing is N/A on this run: it is unattended CI, and this change has no user-visible product surface.

中文说明

代码审查

我在看 diff 之前先写下了自己的做法,结果和这个 PR 基本一致——所以我的时间主要花在核实注释里的那些断言是否真的成立,而不是纠结改动的形态。断言都成立。

最关键的那条断言是对的。rc: 条目追加深链,只有在 rc 去重不读渲染行的前提下才安全,而它确实不读:review_comment 的语料比对是 ($klines | any(test($r.anchor))),锚点是 anchor = "^- rc:<id> "unique_by 对 rc 用的是 [.src, .id]。两者都不碰 .linerv/ic 恰好相反——它们的身份是 key = (.line | normkey)fullkey = (.raw | normkey)——所以在它们后面加后缀,真的会让每一条已落盘的条目都变成"新"条目。不给 rv/ic 加链接是对机制的正确解读,不是随意取舍。同样的道理也覆盖向后兼容:存量约 100 个 tracking issue 里已有的裸格式 rc 行仍然能匹配锚点,所以追加路径不会引发重复潮。

冒号守卫确实做到了它声称的事。 startswith($t + ":") 既不会让 PR #5 的基础标题收养 PR #50 的 issue(…PR #50 不以 …PR #5: 开头),也不会让 PR #50 的 lookup 收养 PR #5 的增强标题 issue。裸标题的精确匹配对存量 issue 依然有效,而且整个 lookup 本来就限定 creator=${AUTOFIX_BOT},人工改过标题的 issue 永远不会被误收养。

净化处理与既有发布点保持一致,escape-site 计数测试从 1 改到 2 也符合实际——脚本里现在正好有两处 gsub("<!--"; …)。按注释说的理由在 jq 里做标题截断(而不是 bash)是正确的。PR_AUTHOR 在到达那唯一一个故意不净化的 @ 之前,已经被字符集校验 ^[A-Za-z0-9-]{1,39}$ 限制住,所以 cc 提及夹带不了别的东西。

没有其他消费方。 我在整个仓库里搜了标题串和正文串:唯一读取它们的就是这个脚本和它自己的测试。docs/design/autofix-growth-audit.md 描述的是流程(单次 upsert、rc-id 去重、token 净化),从未描述标题或正文措辞,所以这里不会有文档失效。

有一点值得确认,因为很容易搞反: gh api -f原始字符串字段,-F 才是带类型推导的那个,所以即使 login 是纯数字,-f "assignees[]=${PR_AUTHOR}" 也不会被转成数字。这与仓库既有的用法一致(autofix-push-and-report.sh-F pr= 正是因为它需要 GraphQL Int)。我还用一次只读 GET 对真实 API 确认了 gh 会把 key[]=value 序列化成数组——recording stub 只能断言 argv,这一点它是证明不了的。

非阻塞观察

  1. 元数据降级是静默的。GET pulls/{PR} 失败但创建成功时,创建前的 gh_err_reset 会抹掉那次拉取的 stderr,本轮只输出 🗂 … new issue #N。没有任何记录说明这个 issue 拿到的是裸标题、没有 assignee。头部契约里的"每条失败路径都要 warn"针对的是持久化,所以这不算违约——但在 PR_JSON 为空时补一行 warning,能让事后看到一个裸标题 issue 时有迹可查,而这恰恰是本 PR 要消除的那种困惑。
  2. 重试的失败形态是重复 issue,而不是丢失 issue。 对预期中的 422("Unassignable user"),GitHub 什么都不会创建,所以去掉 assignee 的重试是干净的,cc 也仍然能触达作者。但如果创建在服务端已成功、客户端却仍看到非零退出——传输中断,或 --jq '.number' 在畸形响应上失败——重试就会为同一个 PR 开出第二个 tracking issue;随后 lookup 收养最新的那个,另一个成为孤儿。概率很低,而且"绝不丢 findings"是本脚本明示的优先级,所以这个取舍站得住。之所以点出来,是因为重复噪音正是本 PR 要治理的东西。
  3. 小的重复。 PR_AUTHOR 非空且非 bot 这个条件写了两遍(cc 一次、创建分支一次),创建调用出现了三次,差别只在 assignees 参数。按仓库的简洁原则,保持现状没问题——除非出现第四种变体,否则我不会去合并它。

测试

这是无人值守的 CI 运行,所以我没有构建或执行本 PR 的任何代码——下面的证据来自 PR 自己的 CI,通过 API 在被审查的那个 commit 上读取了一次。没有轮询:这是快照,不是最终结果。

抓取时刻没有任何 check 是红的。真正 pin 住本次改动的作业——Test (ubuntu-latest, Node 22.x),它运行 scripts/tests/qwen-autofix-workflow.test.js——当时还没出结果,Lint & Static 和集成测试作业也一样,所以没有失败日志可引。macOS 和 Windows 的单测作业是 skipped,对一个 bash + jq 脚本来说这是合理的覆盖范围:只有 ubuntu 这条道会真正执行它。skipped 的 verify / tmux-testing / publish-* 是本工作流自己的受控通道,不是这个 PR 关掉的。

未验证:单测套件自身的结果——抓取时仍在运行。CI 稳定后下方表格会被就地重写,approve 也随之延后。

(CI 表格见上,未在此重复。)

有一条只能靠沙箱验证来落定,stub 证明不了:@qwen-code /verify ——真正不可被 assign 的作者会让第一次创建返回 422、随后去掉 assignee 的重试能把 findings 落盘。测试里这条路径是靠 ASSIGN_FAIL 对任何 argv 含 assignees 的调用退出 1 来驱动的,它断言了恢复分支,但背后没有 GitHub 真实的拒绝语义。PR 声称的其他内容都直接 pin 在录制下来的 argv 上(标题串、assignees[]=someone、cc 提及、discussion_r 链接、两条降级路径、#5#50 的碰撞用例),对于一个职责就是构造这些参数的脚本来说这是很强的判据——而数组语法那个问题我上面已经自己确认过了。这里没有 TUI 界面,所以 /tmux 不适用。本次运行的真实场景终端测试为 N/A:这是无人值守 CI,且本改动没有用户可见的产品界面。

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

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — the mechanism holds up under scrutiny and the tests pin it directly; what is left is observability, one duplicate-issue failure shape, and an ownership question that belongs to whoever owns the autofix loop.

Back to what I wrote before reading the diff: I would have built this the same way, and I could not find a materially simpler path that still solves the stated problem. The part I expected to have to push on — whether appending a deep link is safe against a dedupe mechanism that compares rendered text — turned out to be the part already reasoned through correctly, including the asymmetry between rc (id-anchored, so a suffix costs nothing) and rv/ic (line-anchored, so a suffix would republish every persisted item once). That asymmetry is easy to get wrong and expensive to discover after merge, and the code says why it is doing what it does. I checked it against the jq rather than trusting the comment.

Does it solve something anyone cares about? I confirmed #11074 is live, unassigned, and unreadable, and the description's account of roughly a hundred such issues is consistent with what the mechanism does. So yes — this is not a solution looking for a problem.

Would I curse this in six months? No. The dedupe anchors were extended rather than replaced, so the ~100 existing issues keep getting adopted; the env contract is unchanged, so neither caller needed touching; and every new branch fails toward "what we do today" rather than toward "findings lost". That is the right priority ordering for a script whose entire job is not losing verified work.

What I am still holding, none of it blocking:

  • The silent metadata degradation and the retry's duplicate-issue failure shape, both detailed in the review above. Both are small, and both fail in the direction this script already chose deliberately.
  • The ownership question is the one thing I cannot settle from the diff. Assigning findings that were classified as outside the PR's footprint — usually pre-existing problems in surrounding code — to the PR author is a policy call about who owes follow-up, and for external contributors what actually arrives is a notification about code they never wrote. If the answer is "the author has the most context and the assignment is just a pointer, not a demand", that is a reasonable position and I would merge it as-is. Flagging it because it is a decision rather than an oversight, and it is better made consciously than discovered by the first contributor who gets one.

Approval is deferred until CI lands green on e9a960b280234d08d3f8587045cbf01bbd4f20f5Test (ubuntu-latest, Node 22.x), the job that runs this PR's own suite, was still in flight at fetch time along with Lint & Static and the integration job, so there is no result to attest to yet. Nothing was red. The check table in the review comment updates in place once CI settles, and the approval follows it.

中文说明

Confidence: 4/5 —— 机制经得起推敲,测试也直接 pin 住了行为;剩下的是可观测性、一个"重复 issue"的失败形态,以及一个归属于 autofix 循环 owner 的判断问题。

回到我在看 diff 之前写下的方案:我会用同样的方式实现,也没能找到明显更简单、又能解决所述问题的路径。我原以为需要施压的那一点——在"比对渲染文本"的去重机制后面追加链接是否安全——恰恰是已经想清楚的那一点,包括 rc(以 id 为锚,加后缀毫无代价)与 rv/ic(以渲染行为身份,加后缀会让所有已落盘条目重发一次)之间的不对称。这种不对称很容易搞错,而合并之后再发现代价很高;代码里写清了为什么这么做,我也是对着 jq 核实过、而不是照信注释。

它解决的是有人在意的问题吗?我确认了 #11074 正在线上、无 assignee、不可读,描述里"约一百个这类 issue"的说法与该机制的实际行为一致。所以是的——这不是为方案找问题。

半年后维护它我会骂人吗?不会。去重锚点是扩展而非替换,存量约 100 个 issue 仍会被收养;env 契约未变,两个调用方都不需要改;每条新增分支的失败方向都是"退回今天的行为",而不是"丢掉 findings"。对一个职责就是"不丢失已验证工作"的脚本来说,这个优先级排序是对的。

仍然保留的意见,都不阻塞:

  • 静默的元数据降级,以及重试可能产生重复 issue 的失败形态,上面审查里都写了。两者都很小,而且失败方向都是这个脚本已经明确选择过的。
  • 归属问题是我唯一无法从 diff 里定论的一点。 把被判定为超出该 PR footprint的 finding(通常是周边代码里本来就存在的问题)assign 给 PR 作者,本质上是一个"谁该跟进"的策略决定;而对外部贡献者来说,真正到达他们的是一条关于自己从未写过的代码的通知。如果答案是"作者上下文最多,assign 只是一个指针、不是要求",这是个合理立场,我可以直接合并。之所以点出来,是因为这是一个决定而非疏忽——由人有意识地做出,总比第一位收到这种 issue 的贡献者去发现要好。

approve 延后到 CI 在 e9a960b280234d08d3f8587045cbf01bbd4f20f5 上全绿之后——运行本 PR 自己测试套件的 Test (ubuntu-latest, Node 22.x) 在抓取时仍在进行,Lint & Static 和集成测试作业也一样,所以还没有可供背书的结果。没有任何 check 是红的。CI 稳定后审查评论里的 check 表格会就地更新,approve 随之执行。

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

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

@yiliang114

Copy link
Copy Markdown
Collaborator Author

End-to-end verification against the real GitHub API (pre-merge, since a workflow_dispatch of qwen-autofix.yml would still run the workflow and the staged upsert script from main's trusted checkout — this change only goes live after merge).

I ran the actual script from this branch against a real PR with real review comments on my fork — yiliang114/qwen-code#5, whose three real inline review comments (ids 3304081708 / 3304081722 / 3304081730) were fed in as deferred-findings.json:

Round 1 — create: yiliang114/qwen-code#10

TITLE:     Deferred review findings from PR #5: test
ASSIGNEES: yiliang114          ← real assignment accepted by the API
BODY:      Verified review findings from PR #5 ("test") by yiliang114 … cc @yiliang114. Each rc: item links back …
           - rc:3304081708 `.github/workflows/qwen-code-pr-review.yml`: Verified real: … — [comment](https://github.com/yiliang114/qwen-code/pull/5#discussion_r3304081708)
           (×3, one per real review comment)

The deep links resolve to the actual review comments on that PR.

Round 2 — re-run with the same findings (+ a reworded re-emission of one): silent exit 0, nothing published — the issue was adopted via its marker and all ids deduped against the stored corpus, including the enriched title form.

Round 3 — one genuinely new finding: appended as a single comment to #10 (1 of 1 new), bullets unchanged in shape.

Bonus real-world failure path hit along the way: the fork initially had issues disabled, and the first run exercised the LOST-warning degradation against a real HTTP 410 — findings dumped verbatim to the run log instead of vanishing silently, exactly per the script's contract.

One artifact worth naming for reviewers: when I ran round 2 with a mismatched AUTOFIX_BOT (qwen-code-dev-bot while the issue was created as yiliang114), the creator-filtered lookup found nothing and opened a duplicate. That is the documented lookup semantics (in production the same bot identity both creates and looks up, so the mismatch cannot occur); the duplicate was deleted and the rounds above were re-verified against the single tracking issue.

@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: the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.

Not explored to full depth (tool budget reached): "agent 1d": none — no check was cut short by the ceiling.; "agent 6b": did not execute scripts/tests/qwen-autofix-workflow.test.js in this worktree (no node_modules; install skipped to stay within budget).

中文说明

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

未审查(原文为英文):the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.

未探索到全部深度(达到工具调用预算):"agent 1d"none — no check was cut short by the ceiling."agent 6b"did not execute scripts/tests/qwen-autofix-workflow.test.js in this worktree (no node_modules; install skipped to stay within budget)

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

Comment thread .github/scripts/upsert-deferred-issue.sh
Comment thread .github/scripts/upsert-deferred-issue.sh Outdated
Comment thread .github/scripts/upsert-deferred-issue.sh Outdated
Comment thread .github/scripts/upsert-deferred-issue.sh Outdated
Comment thread .github/scripts/upsert-deferred-issue.sh Outdated
Comment thread .github/scripts/upsert-deferred-issue.sh Outdated
Comment thread .github/scripts/upsert-deferred-issue.sh Outdated
Comment thread scripts/tests/qwen-autofix-workflow.test.js Outdated
Address review round R1 on the deferred-findings tracking issue.

R1-1 (Critical): the assignee-less retry re-POSTed the non-idempotent
issue create. GitHub silently drops assignees on POST
/repos/{owner}/{repo}/issues for users without push access, so for the
external contributors the retry was named for, the first create returns
201 and the retry is dead code. The failures that DO reach it are the
ambiguous transport ones (connection reset, gateway 502, a read timeout
after the server already committed), where the re-POST mints a second
tracking issue carrying the same marker: the next round's newest-first
lookup adopts the newer one and the first is orphaned forever, publicly
duplicating every finding while the round logs clean success. That is
against this file's own rule that creating a duplicate is worse than
deferring persistence one round. Create once, unconditionally, then
assign best-effort with a separate idempotent call whose failure only
warns — the body's cc mention is what actually reaches an external
contributor.

R1-7 collapses by construction: the create call is now spelled once
instead of three times, and the assignability condition is evaluated once
(ASSIGNABLE, set alongside CC) instead of twice.

R1-2 (Critical): dropped the re-added per-test 30s timeout on this file's
heaviest case. scripts/tests/vitest.config.ts configures testTimeout
90_000 precisely because this case (~14s idle, and heavier in this diff)
exhausted 30s on contended release runners, and 93e1597 (#10870)
removed this exact cap two days ago. Each spawnSync already carries its
own 30s child timeout, which bounds the only real hang risk.

R1-8: pinned the PR-title neutralization, which had no test on any stage —
deleting the entity escape shipped green while a fully
contributor-controlled title published a live @-mention under the bot
identity (GitHub decodes &#64; before its mention filter). One case now
pins the entity escape, the [\r\n\t] flatten and the .[0:80] slice.

R1-9: pinned the ic side of the deep-link exemption. Only rv witnessed the
"no suffix for review/issue_comment" invariant, so widening the condition
to issue_comment shipped green while re-rendering every already-persisted
ic item as new — the one-time duplicate wave the script's comment warns
against.

Verified: npx vitest run --config ./scripts/tests/vitest.config.ts
scripts/tests/qwen-autofix-workflow.test.js -> 225 passed | 4 skipped.
Each new assertion reds under its own mutation: retry shape restored,
title entity gsub / flatten / slice deleted, suffix widened to ic.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmto8kxx1l2
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 10800 seconds (of the 180-minute budget). For large PRs, retry with a longer timeout by commenting: @qwen-code /review --timeout=360. See workflow logs.

The pulls-endpoint call was the only gh call in upsert-deferred-issue.sh
whose captured stderr nobody read: `|| true` swallowed the exit status and
the `gh_err_reset` before the create call wiped the reason, so a systematic
fetch failure — a fine-grained PAT rotated without pull-requests:read, a
rate limit, a persistent 404 — silently reverted every new tracking issue
to the bare title / no assignee / no cc while the success line still
printed. That is against this file's own header contract (every failure
path warns and exits 0) and the GH_ERR design note (these warnings are the
feature's only signal): every sibling call routes its reason into a
`gh_reason`-bearing warning.

The gate is the call status, not a body field — `jq -e '.number'` would
warn on every healthy round whose PR JSON carries no `.number`. One
non-blocking warning, read before that reset so the create-failure warning
below still reports its own reason; the bare-title degradation and the
never-fail-a-round contract are unchanged.

Pinned in the existing prFetchFailed case (warning text, reason routed
through gh_reason() so the `::` payload arrives neutralized, no raw
`::error::` on stdout) plus a no-false-warning assertion on the healthy
create path. Mutations observed red, then restored: delete the warning,
read raw ${GH_ERR}, and swap in the `.number` gate.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtolfwlblj

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

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R1-6 two hand-mirrored neutralization chains — already reported (comment 3940140375), thread resolved, follow-up issue 11128 open

Not reviewed: finding R2-9 — the verifier never ruled on it: the round-3 verification that would have was refused by the review time budget.

Not reviewed: the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": shellcheck on the changed script — no shellcheck binary in this worktree and node scripts/lint.js --shellcheck downloads one, so I could not measure whether….

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

⚠️ 1 finding(s) still carried the — [unverified] tag when the loop ended — the verifier never ruled on them, and they are not confirmed.

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

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

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):finding R2-9 — the verifier never ruled on it: the round-3 verification that would have was refused by the review time budget.

未审查(原文为英文):the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"shellcheck on the changed script — no shellcheck binary in this worktree and node scripts/lint.js --shellcheck downloads one, so I could not measure whether…

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

⚠️ 循环结束时仍有 1 条发现带着 — [unverified] 标记——验证者从未对它们作出裁决,它们不算已确认。

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

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

Comment thread .github/scripts/upsert-deferred-issue.sh Outdated
Comment thread .github/scripts/upsert-deferred-issue.sh Outdated
Comment thread .github/scripts/upsert-deferred-issue.sh
Comment thread .github/scripts/upsert-deferred-issue.sh Outdated
Comment thread .github/scripts/upsert-deferred-issue.sh
Comment thread .github/scripts/upsert-deferred-issue.sh
# 201 unassigned. Failure only warns (persistence already succeeded) and
# the body's cc mention is what actually reaches them.
gh_err_reset
gh api "repos/${REPO}/issues/${NUM}/assignees" -f "assignees[]=${PR_AUTHOR}" \

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 create-time assignment has two unaccounted downstream effects in .github/workflows/qwen-autofix.yml, a file this diff does not touch. (A) It is a PAT-authenticated write that fires an issues.assigned webhook, which that workflow listens on, so every first-time deferral starts a route job that always ends in 🧭 issue event ignored. (B) An assignee removes the tracking issue from the scheduled ready-for-agent scan, which filters no:assignee — while the body this same diff writes at :448 invites exactly that flow.

(A) qwen-autofix.yml:25-29 declares on.issues.types: ['labeled','assigned'] and the route job's if: (:213-214) prefilters only issue_comment, pull_request and pull_request_review, so an issues.assigned event passes on github.repository alone; runs-on (:218-221) resolves to the self-hosted ecs-qwen pool that the file says it reserves because "a hosted backlog queued route past the cron period, and af-005's supersede then starved every scan round". GitHub's "events caused by GITHUB_TOKEN do not start a run" suppression does not apply — the upsert child runs with CI_DEV_BOT_PAT (:6582, from a step whose env is secrets.CI_DEV_BOT_PAT at :5942). The job then spends a gh api …/collaborators/{sender}/permission call (:556) to reach 🧭 issue event ignored (:576), because ASSIGNEE_LOGIN is the human author, never the bot (guarded at :444), and a brand-new tracking issue carries no labels. (B) AUTOFIX_ISSUE_EXCLUDES: 'no:assignee -linked:pr …' (:656) is consumed by the every-10-minutes scan at :994-995, so once this call succeeds the issue never re-enters that scan; if the labeled-event run is superseded by the per-issue concurrency group (:224-227, cancel-in-progress: true), dies on the runner, or is skipped by a gate, nothing retries and the deferral waits for a human to notice. release.yml:1406-1414 shows the repo relies on that scan as a backstop ("so that, if the dispatch below fails, the scheduled ready-for-agent scan can still find it"). Both effects land only where the author IS assignable — an org member or collaborator, i.e. precisely the maintainers who run this flow; for external contributors GitHub silently ignores the assignee.

Witness:

witness: not run — the deciding facts are GitHub webhook delivery for a PAT-authored write and Actions
trigger evaluation, which need a live repository and a workflow dispatch; the closest capability is the
live A/B arm, and this review has no disposable repository (QWEN_REVIEW_SCRATCH_REPO unset).
Read in the tree at HEAD instead: qwen-autofix.yml:25-29, :213-214, :218-221, :224-227, :552, :556, :576,
:656, :994-995, :5942, :6582; release.yml:1406-1414.
Two in-tree witnesses that PAT-authored issue events DO start runs here:
  main-ci-failure-issue.yml:191 (GH_TOKEN: secrets.CI_DEV_BOT_PAT) + :205 (--add-assignee "${AUTOFIX_BOT}")
    — a designed trigger that only works if a PAT-authored issues.assigned reaches qwen-autofix.yml:552
  qwen-triage.yml:510-521 — records as measured history that bot-PAT-created tracking issues
    "trigger triaged the bookkeeping issue with a full agent run per deferral" (issue 9264)
  qwen-triage.yml:5 listens on ['opened','edited','reopened'] only — not assigned — so its creator
    guard does not cover this event, and qwen-autofix.yml has no equivalent

The cheapest fix for both symptoms: drop the create-time assignment and keep the body's cc @${PR_AUTHOR} mention — the comment at :467-468 says the mention "is what actually reaches them", and it is the only channel that works for the non-assignable population, so the assignment buys little and costs both the no-op run and the scan eligibility. If the assignment is kept: for (A) mirror the expression-level prefilter the file already uses for its other high-volume no-op events (:214), e.g. (github.event_name != 'issues' || github.event.action == 'labeled' || github.event.assignee.login == (vars.AUTOFIX_BOT_LOGIN || 'qwen-code-dev-bot')); for (B) the body must stop pointing at the ready-for-agent flow for THIS issue and say to file a per-item issue instead, since the tracking issue is no longer scan-eligible.

The fix rests on three existing facts. qwen-autofix.yml:552[[ "${ASSIGNEE_LOGIN}" == "${AUTOFIX_BOT}" ]] && label_is_trigger=true — is a deliberate trigger whose issue is ALSO bot-created (main-ci-failure-issue.yml:191 runs with secrets.CI_DEV_BOT_PAT and :205 does --add-assignee "${AUTOFIX_BOT}"), so a prefilter keyed on the creator alone would kill that path: it must also test the assignee. A fix that keeps the assignment must preserve the "${PR_AUTHOR}" != "${AUTOFIX_BOT}" guard at :444, since assigning an issue to the bot is this repo's takeover trigger. And label_is_trigger at :551 is the only remaining route by which a maintainer can send an already-assigned tracking issue to the agent, so a creator-based guard copied from qwen-triage.yml would close the flow entirely rather than the noise.

Fix witness: scripts/tests/qwen-autofix-workflow.test.js:14235 (expect(created.calls).toContain('-f assignees[]=someone')) and the assignFailed case at :14287-14305 go red when the assign call is removed; the replacement pin is expect(created.calls).not.toContain('/assignees') alongside the surviving expect(created.calls).toContain('cc @someone') at :14238. If the route.if prefilter is added instead, assert that a bot-created issue assigned to a non-bot does not start route while an issue assigned to AUTOFIX_BOT still does — removing either half of the clause must red one of the two.

中文说明

[Suggestion] 这个创建即指派的调用,在本 diff 未改动的 .github/workflows/qwen-autofix.yml 里造成两个未被计入的下游影响。(A)它是以 PAT 认证的写操作,会触发 issues.assigned webhook,而该 workflow 正监听此事件,于是每一次首次 deferral 都会启动一个 route 作业,而该作业总是以 🧭 issue event ignored 结束。(B)一旦 issue 有了 assignee,它就会被排除在每 10 分钟一次的 ready-for-agent 扫描之外(该扫描过滤 no:assignee)——而本 diff 在 448 行写进正文的那句话,恰恰在邀请读者走这个流程。

(A)qwen-autofix.yml:25-29 声明 on.issues.types: ['labeled','assigned'],而 route 作业的 if:(213-214 行)只预过滤 issue_commentpull_requestpull_request_review,因此 issues.assigned 事件仅凭 github.repository 就能通过;runs-on(218-221 行)会解析到自建 ecs-qwen 池——文件注释说明保留该池的原因是"托管队列把 route 排过了 cron 周期,af-005 的 supersede 随后饿死了每一轮扫描"。GitHub 的"由 GITHUB_TOKEN 触发的事件不会启动新运行"这一抑制并不适用:upsert 子进程使用的是 CI_DEV_BOT_PAT(6582 行,来自 5942 行 env 为 secrets.CI_DEV_BOT_PAT 的步骤)。该作业随后花费一次 gh api …/collaborators/{sender}/permission 调用(556 行),最终到达 🧭 issue event ignored(576 行)——因为 ASSIGNEE_LOGIN 是人类作者、永远不是 bot(444 行已守卫),而新建的 tracking issue 不带任何 label。(B)AUTOFIX_ISSUE_EXCLUDES: 'no:assignee -linked:pr …'(656 行)被 994-995 行的每 10 分钟扫描消费,因此本调用一旦成功,该 issue 就再也不会进入那个扫描;如果 labeled 事件触发的那次运行被按 issue 分组的并发组取代(224-227 行,cancel-in-progress: true)、在 runner 上死掉、或被某个门跳过,就没有任何东西会重试,该 deferral 只能等人发现。release.yml:1406-1414 表明本仓库确实把该扫描当作兜底("以便下面的 dispatch 失败时,定时的 ready-for-agent 扫描仍能找到它")。这两个影响都只在作者可被指派时发生——即组织成员或协作者,也正是运行这套流程的维护者;对外部贡献者,GitHub 会静默忽略该 assignee。

建议:两个症状最省事的共同修复是去掉创建即指派,保留正文里的 cc @${PR_AUTHOR} 提及——467-468 行的注释就说明该提及"才是真正能触达他们的方式",且它是唯一对不可指派人群有效的通道,因此指派收益很小,却要同时付出这次空转运行与扫描资格的代价。若保留指派:针对(A),仿照该文件已用于其他高频空转事件的表达式级预过滤(214 行),例如 (github.event_name != 'issues' || github.event.action == 'labeled' || github.event.assignee.login == (vars.AUTOFIX_BOT_LOGIN || 'qwen-code-dev-bot'));针对(B),正文必须不再为这个 issue 指向 ready-for-agent 流程,改为说明应就单条 finding 另开 issue,因为 tracking issue 已不具备扫描资格。

修复约束(三条既有事实):qwen-autofix.yml:552[[ "${ASSIGNEE_LOGIN}" == "${AUTOFIX_BOT}" ]] && label_is_trigger=true 是一个刻意设计的触发路径,其 issue 同样由 bot 创建(main-ci-failure-issue.yml:191 使用 secrets.CI_DEV_BOT_PAT,205 行执行 --add-assignee "${AUTOFIX_BOT}"),因此仅按创建者过滤会杀掉该路径——必须同时判断 assignee。若保留指派,必须保留 444 行的 "${PR_AUTHOR}" != "${AUTOFIX_BOT}" 守卫,因为把 issue 指派给 bot 是本仓库的接管触发条件。而 551 行的 label_is_trigger 是维护者把"已有 assignee 的 tracking issue"交给 agent 的唯一剩余路径,所以照搬 qwen-triage.yml 的按创建者过滤会把整个流程关掉,而不只是关掉噪音。

修复验收:移除指派调用后,scripts/tests/qwen-autofix-workflow.test.js:14235expect(created.calls).toContain('-f assignees[]=someone'))与 14287-14305 行的 assignFailed 用例会变红;替代钉子是 expect(created.calls).not.toContain('/assignees'),同时保留 14238 行的 expect(created.calls).toContain('cc @someone')。若改为添加 route.if 预过滤,则断言"bot 创建、指派给非 bot 的 issue 不会启动 route"而"指派给 AUTOFIX_BOT 的 issue 仍会启动"——删除该子句的任一半都必须让两条断言之一变红。

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

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.

Confirmed from the in-tree evidence (webhook delivery itself was not exercised live — no scratch repository): a PAT-authored issues.assigned event passes the route job's expression-level prefilter, and an assigned tracking issue leaves the no:assignee-filtered ready-for-agent scan. Both candidate fixes, however, sit outside what this loop may land in this PR:

  • Drop the create-time assignment — removes a feature this PR deliberately adds (it is in the PR title). The body's cc @${PR_AUTHOR} is indeed the only channel that reaches the non-assignable population, so the assignment's marginal value is the maintainer population — precisely the population whose assignment triggers your (A) and (B). Whether that trade is worth it is a product call for the author, not this round.
  • Add the route.if prefilter — edits .github/workflows/qwen-autofix.yml, a file this PR never touches and an area this loop may not expand into (CI/verification machinery outside the PR's footprint; the deterministic gate rejects such expansion). Your three constraints are noted and would bind any such change: the prefilter must test the assignee, not just the creator (the main-ci-failure takeover trigger relies on a bot-created, bot-assigned issue); the "${PR_AUTHOR}" != "${AUTOFIX_BOT}" guard must stay; and label_is_trigger must remain a live route for already-assigned issues.

Recommendation for the author: if the assignment is worth keeping, land the prefilter plus the body-text adjustment (stop pointing this issue at ready-for-agent) as its own small PR, where the workflow file is in footprint. Leaving this thread open as the decision point.

中文说明

已根据树内证据确认(webhook 投递本身未实际运行——环境中没有 scratch 仓库):以 PAT 触发的 issues.assigned 事件能通过 route 作业的表达式级预过滤;而带上 assignee 的 tracking issue 会被排除在按 no:assignee 过滤的 ready-for-agent 定时扫描之外。然而两个候选修复都超出本循环在本 PR 内可落地的范围:

  • 去掉创建即指派——等于移除本 PR 刻意加入的功能(就写在 PR 标题里)。正文的 cc @${PR_AUTHOR} 确实是唯一能触达不可指派人群的通道,因此指派的边际价值只在于维护者人群——而恰恰是这群人的指派会触发你提出的 (A) 与 (B)。这个取舍是否值得,是作者的产品决策,不是本轮能定的。
  • 添加 route.if 预过滤——需要编辑 .github/workflows/qwen-autofix.yml,本 PR 从未触碰该文件,且该区域属于本循环不得扩张进入的 CI/验证机制(PR 足迹之外;确定性门禁会拒绝此类扩张)。你给出的三条约束已记录,任何此类改动都必须遵守:预过滤必须判断 assignee 而非仅判断创建者(main-ci-failure 的接管触发依赖"bot 创建且指派给 bot"的 issue);"${PR_AUTHOR}" != "${AUTOFIX_BOT}" 守卫必须保留;label_is_trigger 必须仍是已指派 issue 的可用路径。

给作者的建议:若指派值得保留,请把预过滤和正文措辞调整(不再为该 issue 指向 ready-for-agent 流程)作为一个独立小 PR 落地,那样 workflow 文件才在足迹内。此线程保持开放,作为决策点。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not fixing this in this PR, and deliberately leaving the thread open rather than closing it with a promise - it needs an author/maintainer decision, not a review-fix pass.

On (B), because it decides the shape: create-time assignment IS this PR's stated purpose ("...with PR context and assignment"). The finding's "cheapest fix for both symptoms" is to drop it, which removes the feature the PR exists to add - that is not a call to make silently in a fix commit. The scan-eligibility consequence is real but is the intended trade: the tracking issue now has a named owner, so the every-10-minutes no:assignee ready-for-agent scan is no longer its delivery mechanism, and the labeled route (label_is_trigger at :551) remains - as the finding itself confirms is the surviving path for an already-assigned issue.

On (A): the wasted route job is a genuine cost, but the fix lands in .github/workflows/qwen-autofix.yml, which this diff does not touch, and the finding's own constraints make it non-local - a creator-based prefilter would kill the main-ci-failure-issue.yml:191/:205 PAT-authored --add-assignee "${AUTOFIX_BOT}" trigger, so the clause must also test the assignee, and getting that wrong silently closes this repo's takeover path. Changing autofix triggering repo-wide deserves its own review with a live-repo witness. This finding records witness: not run - the deciding facts (PAT-authored webhook delivery, Actions trigger evaluation) were read in-tree, not measured, and I could not measure them here either; gh search issues for an existing follow-up on issues.assigned route noise returned nothing, so there is no tracked issue to point at, and I am not minting one for an unmeasured no-op job.

What did change this round is adjacent and shrinks the blast radius of the notification this assignment sends: as of 3e05ec6 the contributor-controlled PR title no longer reaches the markdown-rendered body at all, so the body an assigned author is notified about can no longer carry an attacker-chosen link, an image beacon, or an unclosed <details> that folds the findings away. The cc @${PR_AUTHOR} mention is unaffected and remains charset-validated.

Decision left open, either defensible and both outside this diff's footprint: (i) keep the assignment and add the assignee-aware route.if prefilter to qwen-autofix.yml as its own change with a live witness; or (ii) drop the assignment and keep only the body's cc @author - which also means rewording the body, since it currently invites the ready-for-agent flow for an issue that would then stay scan-eligible.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Split into the two halves, because they land differently.

(B) — fixed in 239efbf. The body this script writes invited the ready-for-agent flow without saying which issue, and the tracking issue is the one place it has no backstop: AUTOFIX_ISSUE_EXCLUDES (qwen-autofix.yml:656, consumed by the scheduled scan at :995) starts with no:assignee, so once the assign at upsert-deferred-issue.sh:506 lands the issue never re-enters that scan — while release.yml:1487 shows the repo counting on it ("if the dispatch below fails, the scheduled ready-for-agent scan can still find it"). The sentence now attaches the flow to the per-item issue a human files from the list; that issue is unassigned, so it keeps the scan. The script carries a comment saying why, and the upsert case pins it per call (toContain('apply the ready-for-agent flow to that issue') at scripts/tests/qwen-autofix-workflow.test.js:14336, plus not.toContain('(or apply the ready-for-agent flow)') at :14338). Mutation-verified: restoring the old parenthetical reds the case.

(A) — confirmed real, left for a maintainer. Re-verified at the head this reply is written against, current line numbers:

  • on.issues.types: ['labeled','assigned']qwen-autofix.yml:26-28
  • route.if prefilters only issue_comment / pull_request / pull_request_review:214, so an issues.assigned event passes on github.repository alone and lands on the self-hosted ecs-qwen pool (:218)
  • ASSIGNEE_LOGIN env :262; label_is_trigger :550-552; one collaborators/{sender}/permission call :556; exits at 🧭 issue event ignored :576
  • the write is PAT-authenticated, so the GITHUB_TOKEN suppression does not apply: run_deferred_upsert (.github/scripts/autofix-push-and-report.sh:422) passes GITHUB_TOKEN="${GITHUB_TOKEN}" into the clean child, and the Push and report step that executes it (qwen-autofix.yml:5852) sets that to secrets.CI_DEV_BOT_PAT
  • per-issue concurrency group with cancel-in-progress :223-226

Neither exit belongs in this diff:

  1. Drop the create-time assignment, keep only cc @author. That reverses what this PR's title promises ("…and assignment") and the behaviour six pinned cases assert (-f assignees[]=someone at :14321 and :14474, plus the not.toContain('assignees') degradation guards).
  2. Add an expression-level prefilter to route.if. That edits the routing gate of shared production CI for every autofix route, and it cannot be keyed on the creator alone: :552's ASSIGNEE_LOGIN == AUTOFIX_BOT is a deliberate takeover trigger whose issue is also bot-created and bot-assigned under the same PAT (main-ci-failure-issue.yml:191 + :205 --add-assignee "${AUTOFIX_BOT}"), so a creator guard copied from qwen-triage.yml would close that path instead of the noise.

Cost as shipped is one no-op route job per first-time deferral, and only where the author is assignable. Leaving this thread open: (A) needs a maintainer to pick between those two, and I did not touch qwen-autofix.yml.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Filed as #11214 so this survives the merge — a PR review thread is not a durable home for a decision that outlives the PR.

Re-verified at head 1b9b671d3f, current line numbers (the origin/main merge shifted a few from the original finding):

  • the write itself: upsert-deferred-issue.sh:506 (ASSIGNABLE == 1 gate) → :514 (gh api repos/${REPO}/issues/${NUM}/assignees -f "assignees[]=${PR_AUTHOR}")
  • it is PAT-authenticated, so the GITHUB_TOKEN suppression does not apply: autofix-push-and-report.sh:422-432 passes GITHUB_TOKEN="${GITHUB_TOKEN}" into the clean child, and the enclosing step's env sets that to secrets.CI_DEV_BOT_PAT (qwen-autofix.yml:813)
  • on.issues.types: ['labeled','assigned']qwen-autofix.yml:26-28; route.if prefilters only issue_comment / pull_request / pull_request_review:214, with no issues clause, so the event passes on github.repository alone and lands on the reserved self-hosted ecs-qwen pool (:218)
  • it spends one collaborators/{sender}/permission call (:556) and exits at 🧭 issue event ignored (:576), because label_is_trigger is false — no labels on a brand-new issue (:551) and ASSIGNEE_LOGIN is the human author, not the bot (:552)

(B) is no longer open: it was fixed here in 239efbfc9b, which points the ready-for-agent hint at the per-item issue a human files (unassigned, so it keeps the no:assignee scan backstop at qwen-autofix.yml:656 / :994-995) instead of at this now-assigned tracking issue. Only (A) remains.

(A) is deliberately not fixed in this diff, for the reason the dev-bot gave: this PR's footprint is .github/scripts/upsert-deferred-issue.sh + scripts/tests/qwen-autofix-workflow.test.js and nothing else. It never touches .github/workflows/qwen-autofix.yml, and changing autofix triggering repo-wide from a PR about deferred-findings issue bodies is exactly the expansion the deterministic gate rejects. I am not contradicting that adjudication — I am giving it a tracked home.

Its three constraints are recorded in #11214 as binding, and I re-confirmed all three in-tree rather than copying them:

  1. the prefilter must test the assignee, not just the creator — :552's ASSIGNEE_LOGIN == AUTOFIX_BOT is a deliberate takeover trigger whose issue is also bot-created under the same PAT (main-ci-failure-issue.yml:191 + :205 --add-assignee "${AUTOFIX_BOT}"), so a creator-only guard would close this repo's takeover path instead of the noise
  2. the "${PR_AUTHOR}" != "${AUTOFIX_BOT}" guard at upsert-deferred-issue.sh:479-483 stays
  3. label_is_trigger (:550-551) stays a live route for already-assigned issues — which matters more now that (B) makes this tracking issue permanently scan-ineligible

#11214 also carries the two mutually exclusive exits (add the assignee-aware prefilter vs drop the assignment and keep only cc @author) as a maintainer product decision, and keeps the honest witness caveat: the deciding facts are PAT-authored webhook delivery and Actions trigger evaluation, which were read in-tree, not measured. A fix PR should bring a live-repo witness.

Not resolving this thread. Both the dev-bot and my earlier reply on it designated it the open decision point, and (A) still has no chosen exit — resolving it would record a decision nobody has made. #11214 is where the decision gets made; this thread stays open until it does.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-verified at head 439cd8e872 — the head has moved past 1b9b671d3f, which the last reply on this thread was written against. No code changed this round, and the split that reply made still holds.

(B) — still fixed. 239efbfc9b is an ancestor of head (git merge-base --is-ancestor 239efbfc9b HEAD → yes). .github/scripts/upsert-deferred-issue.sh:485-493 points the ready-for-agent flow at the per-item issue a human files from the list, not at this now-assigned tracking issue, so the no:assignee scan backstop (qwen-autofix.yml:656, consumed by the scheduled scan at :995) survives on the issue that needs it.

(A) — still real, still human-gated, still deliberately not fixed in this diff. Re-confirmed in-tree at current line numbers rather than carried over from the earlier replies:

  • on.issues.types includes assignedqwen-autofix.yml:26-28
  • route.if (:214) has no issues clause at all — it prefilters only issue_comment, pull_request and pull_request_review — so an issues.assigned event passes on github.repository alone and lands on the reserved self-hosted ecs-qwen pool (:218)
  • it then spends one collaborators/{sender}/permission call (:556) and exits at 🧭 issue event ignored (:576), because label_is_trigger is false: a brand-new tracking issue carries no labels (:551) and ASSIGNEE_LOGIN is the human author, never the bot (:552; the assignment is gated at upsert-deferred-issue.sh:480-483, whose "${PR_AUTHOR}" != "${AUTOFIX_BOT}" guard is intact)
  • the write is PAT-authenticated, so GitHub's "events caused by GITHUB_TOKEN do not start a run" suppression does not apply: the enclosing step's env is secrets.CI_DEV_BOT_PAT (qwen-autofix.yml:813) and run_deferred_upsert passes GITHUB_TOKEN through into the clean child

Both exits remain outside what this round may land:

  1. Drop the create-time assignment — reverses the feature this PR's title promises and that the pinned cases assert.
  2. Add the assignee-aware prefilter to route.if — edits the routing gate of shared production CI in a file this PR never touches, and it cannot be keyed on the creator alone: :552's ASSIGNEE_LOGIN == AUTOFIX_BOT is a deliberate takeover trigger whose issue is also bot-created under the same PAT (main-ci-failure-issue.yml:191 + :205 --add-assignee "${AUTOFIX_BOT}"), so a creator-only guard would close this repo's takeover path instead of the noise.

That is a maintainer decision about repo-wide autofix triggering, not a review-fix pass, and it is tracked in #11214 (open, priority/P2, status/waiting-for-feedback) with all three of this finding's constraints recorded as binding.

Not resolving: #11214 has no chosen exit yet, and both an earlier dev-bot reply and the author's replies on this thread designated it the open decision point — resolving would record a decision nobody has made. Scope this round: 0 files changed, worktree still at 439cd8e872, no push.

@wenshao

wenshao commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

@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 Sep 5, 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. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

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

中文说明

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

…e create-path guards

Address review round R2 on the deferred-findings tracking issue.

R2-1: the one neutralized PR_TITLE was published to two surfaces with
different renderers. The issue TITLE is stored and rendered as plain
text — no markdown pass, no mention filter — so the entity/@/comment-
opener stages protect nothing there and only corrupt the string this
change exists to make readable (a ZWSP inside `@types` on a routine
dependency-bump title, visible `\-\-` backslashes). Derive two variants
from the one fetch: PR_TITLE_RAW (flatten + cap only) for CREATE_TITLE,
and the fully neutralized PR_TITLE for the markdown-rendered body copy.
The gsub("<!--") census stays at 2 — the raw variant adds no site.

R2-2: the PR_AUTHOR login-charset guard had no witness — deleting it
shipped green while a malformed `.user.login` flowed into both the
deliberate `cc @…` mention in the public body and the `assignees[]`
argument. New badLogin case.

R2-5: the `-n "${NUM}" &&` half of the assign guard had no witness —
dropping it let the create-failure path POST repos/…/issues//assignees
and print a could-not-assign warning immediately before the LOST
warning. Pinned on the existing writeFail case.

R2-3: the enriched-title format was written at creation and re-spelled
220 lines away at the marker-less adoption lookup, each side pinned by
its own hand-typed literal — a one-sided restyle (": " → " - ") left
the suite green while production forked a second tracking issue. Added
a round-trip case that derives the adoption fixture from the recorded
create call, plus a comment at the create site naming the lookup as the
separator's consumer.

Mutation probes observed red, then restored: title reverted to the
escaped variant; body copy swapped to the raw variant; body-chain
flatten deleted; charset guard deleted; NUM guard half dropped;
separator restyled on the create side (including the maintainer
scenario — every hand-typed create-side literal updated, round-trip
still reds) and on the lookup side.

Verified: npx vitest run --config ./scripts/tests/vitest.config.ts
scripts/tests/qwen-autofix-workflow.test.js -> 229 passed.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@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 轮)。改动内容与我反驳保留之处如下:

Address-review round — PR #11080

Commit: aa6a7f4aaffix(ci): keep the enriched tracking-issue title plain-text and pin the create-path guards

This round covered the round-2 review findings. Four Suggestions were implemented (one code change plus three test-witness additions), three items stay open as decisions for the author. Diff growth: script +14/−3, tests +86/−19 — within the window budgets.

Implemented

  • R2-1 — one neutralized PR_TITLE published to two different renderers ([rc:3941924806]). The issue TITLE is stored and rendered as plain text — no markdown pass, no mention filter — so the entity/@/comment-opener stages protected nothing there and only corrupted the string (a ZWSP inside @types on a routine dependency-bump title; visible \-\- backslashes). The script now derives two variants from the one fetch: PR_TITLE_RAW (flatten + cap only) for CREATE_TITLE, and the fully neutralized PR_TITLE for the markdown-rendered body copy. The gsub("<!\-\-") census stays at 2 (the raw variant adds no site) and the bothSyntaxes census is untouched. The titled and entityTitle cases now assert per call (raw text against the -f title= call, escaped text against the -f body= call) so dropping either half of the derivation reds one of them.
  • R2-2 — login-charset guard had no witness ([rc:3941924808]). New badLogin case ("user":{"login":"not a login"}) asserting tracked in new issue #77, no assignees, no cc @. The reviewer's equivalent-mutant correction was heeded: a no-.user case pins nothing the default doesn't, so only the rejected-login case was added.
  • R2-5 — -n "${NUM}" && half of the assign guard had no witness ([rc:3941924818]). Added expect(writeFail.calls).not.toContain('assignees') to the existing writeFail case, mirroring prFetchFailed's author-gate pin. Added on the case's calls, not the stub, per the reviewer's constraint (assignFailed requires exactly one recorded assignment).
  • R2-3 — create-side title format not tied to the adoption lookup ([rc:3941924811]). Chose the round-trip test over a shared TITLE_SEP variable (test-only, and it exercises adoption behavior end-to-end rather than restating the string): a new case derives the lookup fixture title from the recorded create call and asserts the marker-less issue is adopted (issues/46/comments -f body=, no second -f title=). Plus one comment line at the create site naming the lookup as the separator's consumer.

Left open — decisions for the author

  • R1-5 — contributor-controlled markdown/HTML surface in the body ([rc:3941924802]). Re-probed and confirmed on this head; still a product call. This round's R2-1 split deliberately does not close it (the body copy still renders markdown), and it makes option 3 (fail-closed drop of the body copy) cheaper: the enriched issue title now carries the raw PR title, so the self-describing goal no longer depends on the body copy. The three options with trade-offs are laid out in the thread reply; the proposed [x](https://evil.example/y) pin stays unadded because it can only go green once an option is chosen.
  • R2-4 — stale PR description ([rc:3941924815]). Real and confirmed, but editing the PR body is a GitHub write this loop cannot perform (no credentials in the fix sandbox). The thread reply carries ready-to-paste replacement text for all four stale passages plus the Evidence line, for both language halves.
  • R2-6 — create-time assignment's downstream effects ([rc:3941924819]). Both candidate fixes are out of bounds for this round: dropping the assignment removes a deliberate feature (author's product call), and the route.if prefilter edits .github/workflows/qwen-autofix.yml, which this PR never touched and which sits in the CI-machinery area this loop may not expand into. Recommended: land the prefilter plus body-text adjustment as its own small PR if the assignment is kept.

Resolved earlier-round findings, re-verified at this head

R1-1 (single create + separate assign), R1-2 (no per-test 30 s cap on the heaviest case — the remaining }, 30000); lines belong to other tests), R1-4 (fetch-failure warning), R1-7 (one create-call site), R1-8 (title-witness case, extended this round), R1-9 (ic-side witness) — all still hold; the six threads are listed in resolved-comments.txt.

Verification

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js229 passed (229) (full file, run after the final edit).
  • Same command with -t 'upserts deferred findings into a per-PR issue'1 passed | 228 skipped, re-run after the commit against the committed tree.
  • Mutation probes (each applied, observed red, then restored): title reverted to the escaped variant → red at the titled per-call assertion; body copy swapped to the raw variant → red at @\u200bfoo in body; body-chain flatten deleted → red at the …&commat;x AAA body assertion (witness added during this round's self-audit); charset guard deleted → badLogin red (assignees[]=not a login observed); -n "${NUM}" && dropped → writeFail red (repos/o/r/issues//assignees observed); create-side ": "" - " with all hand-typed create-side literals updated (the maintainer trap scenario) → round-trip red at issues/46/comments; lookup-side ":"" -" → red (enrichedAdopted and the round-trip).
  • npm run typecheckpassed (exit 0). Note: the run required first rebuilding packages/web-shell and packages/web-templates, whose dist/ outputs were missing in this sandbox's workspace (their absence is also what made the first typecheck attempt fail on @qwen-code/web-templates resolution — an environment state issue, unrelated to this diff).
  • npm run buildcould not complete as one command in this sandbox: every attempt was killed by the environment's ~120 s per-command ceiling mid-sequence (and two killed attempts left packages/web-templates/dist half-cleaned, which I rebuilt afterwards). Every workspace in the build order was instead built successfully individually (exit 0 each): webui, web-shell, web-templates, cli (bundle ✓), qwen-live, vscode-ide-companion, chrome-extension, integrations/external-context, integrations/external-context-mem0; the earlier full-run segments had already completed generate, core, all channels/*, audio-capture, node-repl, acp-bridge, and sdk-typescript. The diff touches no workspace source (a shell script and a JS test), so the full command's green is carried by the gate's own runner.
  • npm run lint — same ceiling; not completed as one command. npx eslint scripts/tests/qwen-autofix-workflow.test.jspassed (exit 0). The lint command's filter is .ts/.tsx and this diff touches none, so the full-run result is inherited from base.
  • bash -n .github/scripts/upsert-deferred-issue.shclean. (shellcheck is not installed in this sandbox, as the review already disclosed; the change adds no new expansion except the PR_TITLE_RAW jq call mirroring the existing one.)
  • Environment repair performed during verification: one npm install --no-audit --no-fund --cache /tmp/npm-cache to restore the missing nested packages/webui/node_modules/vite-plugin-dts (without it packages/webui's build cannot run). package-lock.json was restored byte-identical with git checkout afterwards — the commit contains no lockfile change.
中文说明

Address-review 轮次 — PR #11080

提交:aa6a7f4aaffix(ci): keep the enriched tracking-issue title plain-text and pin the create-path guards

本轮处理第 2 轮评审的发现。实施了 4 条 Suggestion(一处代码修改加三处测试见证补齐),3 条作为作者决策保持开放。diff 增长:脚本 +14/−3,测试 +86/−19——在窗口预算内。

已实施

  • R2-1 — 同一份净化后的 PR_TITLE 发布到两个渲染器不同的落点([rc:3941924806])。issue 标题按纯文本存储与渲染——不做 markdown 解析、没有提及过滤器——因此实体/@/注释起始符三个阶段在标题面上什么 也保护不了,只会破坏字符串(常规依赖升级标题里 @types 中夹入零宽空格;出现可见的 \-\- 反斜杠)。脚本现在从同一次拉取派生两个变体:PR_TITLE_RAW(仅压平+截断)用于 CREATE_TITLE,完整净化的 PR_TITLE 用于渲染 markdown 的正文副本。gsub("<!\-\-") 计数保持为 2(raw 变体不新增站点),bothSyntaxes 计数未动。titledentityTitle 用例现在按调用分别断言(-f title= 调用上断言原始文本、-f body= 调用上断言转义文本),因此删掉拆分派生的任一半都会让其中一条变红。
  • R2-2 — 登录名字符集守卫没有见证([rc:3941924808])。新增 badLogin 用例("user":{"login":"not a login"}),断言 tracked in new issue #77、无 assignees、无 cc @。采纳了评审者的等价变异体更正:无 .user 的用例钉不住任何默认值之外的东西,因此只加了"登录名被拒"用例。
  • R2-5 — 指派守卫中 -n "${NUM}" && 一半没有见证([rc:3941924818])。在既有 writeFail 用例中加入 expect(writeFail.calls).not.toContain('assignees'),与 prFetchFailed 的作者门钉子相对应。按评审者约束,断言加在用例的 calls 上而非修改 stub(assignFailed 要求恰好记录一次指派)。
  • R2-3 — 创建侧标题格式与收养查找逻辑互不绑定([rc:3941924811])。选择了往返测试而非共享 TITLE_SEP 变量(仅改测试,且端到端检验收养行为而非复述字符串):新用例从记录的创建调用派生查找 fixture 标题,断言 marker 缺失的 issue 被收养(issues/46/comments -f body=,且无第二个 -f title=)。另在创建处加一行注释,指明查找逻辑是该分隔符的消费方。

保持开放 — 待作者决策

  • R1-5 — 正文中贡献者可控的 markdown/HTML 面([rc:3941924802])。在本 head 上重新探测并确认;仍是产品决策。本轮的 R2-1 拆分刻意没有关闭它(正文副本仍渲染 markdown),但让方案 3(fail-closed 移除正文副本)代价更低:增强后的 issue 标题现在携带原始 PR 标题,"一眼可辨识"的目标不再依赖正文副本。三个方案及取舍已在线程回复中列出;你提议的 [x](https://evil.example/y) 验收用例仍未加入,因为只有选定方案后它才能转绿。
  • R2-4 — PR 描述过期([rc:3941924815])。属实并已确认,但编辑 PR 描述是 GitHub 写操作,本循环无法执行(修复沙箱没有凭据)。线程回复中附有四处处过期段落加 Evidence 行的现成替换文本,中英文两半均备。
  • R2-6 — 创建即指派的下游影响([rc:3941924819])。两个候选修复都超出本轮边界:去掉指派等于移除刻意加入的功能(作者的产品决策);route.if 预过滤需要编辑 .github/workflows/qwen-autofix.yml——本 PR 未触碰该文件,且该区域属于本循环不得扩张进入的 CI 机制。建议:若保留指派,把预过滤与正文措辞调整作为独立小 PR 落地。

已解决的更早轮次发现 — 在本 head 重新验证

R1-1(单次创建+独立指派)、R1-2(最重用例无 30 秒单测超时——残留的 }, 30000); 行属于其他测试)、R1-4(拉取失败告警)、R1-7(单一创建调用点)、R1-8(标题见证用例,本轮已扩展)、R1-9(ic 侧见证)——均仍然成立;六个线程已列入 resolved-comments.txt

验证

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js229 passed (229)(最后一次编辑后跑了完整文件)。
  • 同一命令加 -t 'upserts deferred findings into a per-PR issue'1 passed | 228 skipped,提交后针对已提交树再跑过一次。
  • 变异探测(每次施加、观测到变红、随后恢复):标题回退为转义变体 → titled 按调用断言变红;正文副本换为 raw 变体 → 正文 @\u200bfoo 变红;删除正文链的压平 → 正文 …&commat;x AAA 断言变红(本轮自审时补的见证);删除字符集守卫 → badLogin 变红(观测到 assignees[]=not a login);去掉 -n "${NUM}" &&writeFail 变红(观测到 repos/o/r/issues//assignees);创建侧 ": "" - "所有手写创建侧字面量同步更新(维护者陷阱场景)→ 往返用例在 issues/46/comments 变红;查找侧 ":"" -" → 变红(enrichedAdopted 与往返用例)。
  • npm run typecheck通过(退出码 0)。注意:该运行需要先重建 packages/web-shellpackages/web-templates——它们的 dist/ 产物在本沙箱工作区中缺失(这也是首次 typecheck 在 @qwen-code/web-templates 解析上失败的原因——属环境状态问题,与本 diff 无关)。
  • npm run build在本沙箱中无法作为单条命令跑完:每次尝试都在中途被环境的约 120 秒单命令上限杀掉(其中两次被杀的尝试把 packages/web-templates/dist 清了一半,我随后已重建)。改为按构建顺序逐个工作区单独构建,全部成功(各自退出码 0):webuiweb-shellweb-templatescli(打包 ✓)、qwen-livevscode-ide-companionchrome-extensionintegrations/external-contextintegrations/external-context-mem0;更早的完整运行段落已完成 generatecore、全部 channels/*audio-capturenode-replacp-bridgesdk-typescript。本 diff 不触碰任何工作区源码(一个 shell 脚本和一个 JS 测试),完整命令的绿色由门禁自己的 runner 承载。
  • npm run lint — 受同样上限限制,未能作为单条命令跑完npx eslint scripts/tests/qwen-autofix-workflow.test.js通过(退出码 0)。lint 命令的过滤范围是 .ts/.tsx,本 diff 不涉及,因此完整运行结果继承自 base。
  • bash -n .github/scripts/upsert-deferred-issue.sh干净。(本沙箱未安装 shellcheck,评审已披露;本改动除镜像既有写法的 PR_TITLE_RAW jq 调用外不新增任何展开。)
  • 验证过程中进行过一次环境修复:运行了一次 npm install --no-audit --no-fund --cache /tmp/npm-cache,以恢复缺失的嵌套依赖 packages/webui/node_modules/vite-plugin-dts(没有它 packages/webui 无法构建)。随后用 git checkoutpackage-lock.json 恢复为逐字节一致——本次提交不含 lockfile 改动

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

🧵 Resolved all 10 selected review thread(s). · 已关闭全部选中的 10 条评审线程。

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/模型 kimi-k3

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

Reviewed. Suggestions are inline.

3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R1-5 unbounded markdown surface in the PR-title neutralization — already reported (comment 3941924802)
  • R2-4 PR description still specifies the removed assignee-less create retry — already reported (comment 3941924815)
  • R2-6 create-time assignment's two downstream effects in qwen-autofix.yml — already reported (comment 3941924819)

Not explored to full depth (tool budget reached): "agent 6b": could not execute scripts/tests/qwen-autofix-workflow.test.js (no node_modules in this review worktree), so the greenness of the new assertions and the case…; "agent 3c": none — I completed the walk within budget. I did not execute the 25k-line test file (expensive, and greenness/portability is the build-lint lane's concern, not ….

Convergence: round 3 posted 4 inline comment(s), 4 of them reported for the first time; the previous round posted 7 (6 new). Findings keep coming back to the same files: .github/scripts/upsert-deferred-issue.sh (findings in rounds 1, 2; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

已审查。 建议见行内评论。

本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未探索到全部深度(达到工具调用预算):"agent 6b"could not execute scripts/tests/qwen-autofix-workflow.test.js (no node_modules in this review worktree), so the greenness of the new assertions and the case…"agent 3c"none — I completed the walk within budget. I did not execute the 25k-line test file (expensive, and greenness/portability is the build-lint lane's concern, not …

收敛情况:第 3 轮发布了 4 条行内评论,其中 4 条是首次提出;上一轮发布了 7 条(其中 6 条首次提出)。发现反复回到同一批文件:.github/scripts/upsert-deferred-issue.sh(第 1、2 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment thread .github/scripts/upsert-deferred-issue.sh Outdated
Comment thread scripts/tests/qwen-autofix-workflow.test.js
Comment thread scripts/tests/qwen-autofix-workflow.test.js
Comment thread scripts/tests/qwen-autofix-workflow.test.js
…ning shapes

Three review findings on this PR:

- R3-1 (.github/scripts/upsert-deferred-issue.sh): the degradation warning was
  gated on the gh call status alone. This fetch passes no --jq, and gh copies a
  non-JSON body raw with serverError set only above status 299, so a transparent
  proxy answering `200 text/html` exits 0 while all three derivations come back
  empty through their `|| true` - the bare title / no cc / no assignee
  degradation behind a clean success line, which is exactly what the warning
  exists to name. Extend the gate to "the call failed OR both derived strings
  are empty" and name that second case in the message, instead of letting
  gh_reason() read "no stderr captured" for a call that succeeded. Keying on
  both strings being empty cannot false-positive on a real PR object (a
  whitespace-only title still flattens to a non-empty " ", a deleted author
  leaves the title), where a `.number`-presence gate would warn on every
  healthy round.
- R3-2 (scripts/tests): nothing pinned the fetch's ABSENCE on the append path,
  so hoisting the 4-line fetch unit above the create/append branch shipped green
  while costing an authenticated GET /pulls/N against the bot PAT's rate limit
  on every steady-state append round - the path that runs most often - instead
  of once per tracking issue's lifetime. Add the negative half to the existing
  `appended` case, which already pins the append-only shape.
- R2-2 (scripts/tests): `badLogin` exercised only the space shape of the four
  malformed-login shapes its own comment declares the charset guard covers, so
  widening `{1,39}` to `{1,}` or admitting `@` into the class both shipped green
  while a 40-character or `@`-bearing login flowed into the deliberately
  un-defused public `cc @...` mention and into an argv element. Add one fixture
  per remaining shape.

Mutation-verified against the whole file: dropping the gate's second half,
dropping the exit-0 reason wording, hoisting the fetch unit above the branch,
and each of the two charset widenings reds the suite (1 failed each); intact
tree is 225 passed / 4 skipped. shellcheck note count goes 10 -> 9, since
$(gh_reason) moved out of the masked echo into a plain assignment.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtp6vibomd
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (timeout (3600000ms)) (attempt 2/100) — it will retry on the next scan.

⚠️ This change was NOT pushed — any commit referenced below was made only in the runner workspace and has been discarded. What the agent reported:
Qwen failed during address-review: timeout (3600000ms).

See the Qwen Autofix agent step logs for model/tool output.

中文说明

🤖 AutoFix 在完成前耗尽了时间(timeout (3600000ms))(第 2/100 次尝试)—— 将在下次扫描时重试。

Run log: https://github.com/QwenLM/qwen-code/actions/runs/34005069100


🧠 Handled by Qwen Code · model/模型 kimi-k3

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

3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R1-5 unbounded markdown surface in the PR-title neutralization — already reported (comment 3941924802)
  • R2-6 create-time assignment's two downstream effects in qwen-autofix.yml — already reported (comment 3941924819)
  • the ": " title separator hand-spelled at the create and adoption sites instead of one shared constant — already reported (comment 3941924811)

Not reviewed: the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.

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

  • scripts/tests/qwen-autofix-workflow.test.js:14351 — [review] The assign warning's captured reason is never asserted
  • .github/scripts/upsert-deferred-issue.sh:425 — [review] Login guard is a weaker copy of the repo's canonical one

Convergence: round 4 posted 2 inline comment(s), 1 of them reported for the first time; the previous round posted 4 (4 new). Findings keep coming back to the same files: .github/scripts/upsert-deferred-issue.sh (findings in round 3; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

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

本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.

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

收敛情况:第 4 轮发布了 2 条行内评论,其中 1 条是首次提出;上一轮发布了 4 条(其中 4 条首次提出)。发现反复回到同一批文件:.github/scripts/upsert-deferred-issue.sh(第 3 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment thread .github/scripts/upsert-deferred-issue.sh
Comment thread scripts/tests/qwen-autofix-workflow.test.js
@wenshao

wenshao commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (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: 1019 passed · 1 failed · 1020 total

Flakiness gate: ⚠️ timeout — only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

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

脚本断言:1019 通过 · 1 失败 · 1020 总计

抖动门:⚠️ timeout — only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed

Verification report

PR #11080 — deep verification

Verdict: findings — 1020 scripted assertions, 1019 pass / 1 fail. Verified head ba2b9851234283e15d51d1bac21d8129acab107d (git rev-parse HEAD^2), base 1b604721b053da4e104ff610e1a925fe8c9ca488 (HEAD^1).

The central claim is proven load-bearing by A/B against the base build and survives an 855-assertion hostile-input sweep. The one fail is not a behavioural defect: it is the mutation matrix catching a test-coverage gap on the property the test file itself ranks worst (never retry the create), where a comment in the new tests overstates what its own assertion pins. A second, lower-severity finding is measured test-runtime growth against a fixed budget. Neither blocks the behaviour; both are worth a reviewer's eyes.

中文摘要

结论:findings —— 1020 条脚本化断言,1019 通过 / 1 失败。验证 head ba2b9851,base 1b604721

A/B 结论:中心主张成立且是 load-bearing 的。用独立的 recording-gh harness,在 workflow 真实的 env -i 契约下跑真实脚本(head 与 base 两臂),80/80 断言通过:head 产出增强标题 / cc / 独立 assign / rc 深链,base 全部没有;所有降级臂(fetch 失败、fetch 退出 0 但正文不可用、assign 被拒、bot 作者、畸形 login)都仍然落盘 findings 并给出告警。见 01-ab-head-vs-base-cells.png

Findings(详见下文 Findings 一节):

  1. create 永不重试 这条性质没有任何测试 pin 住。变异体 M8(失败后重试一次 create)在 PR 自带套件下 survived,而它是 behaviourally live 的:head 发 1 次 create POST,变异体发 2 次且 marker/标题完全相同(正是脚本注释描述的重复 issue 机制),两臂 stdout 逐字节相同。测试注释声称"恢复重试形态必须让下面的 single-create 断言变红",但那条断言在 assignFailed 里,而它的 create 是成功的。见 04-m8-survivor-1-vs-2-create-posts.png
  2. 该文件最重的用例从 base 的 37.6s 涨到 head 的 53.9s(+43%,另一次运行 61.6s),预算是 testTimeout: 90_000,余量只剩 1.46–1.67x。PR 自己新增的注释就引用了两个"30s 上限在争抢的 release runner 上超时"的 run id。

Corrections:描述里"该用例实测 ~4.8s"在本容器不成立(base 就已经 37.6s);一条新测试注释称 perSource 只 pin 住 rv 侧,实测 M1 是被 perSource 自己既有的断言(14769 行)先打红的。

未覆盖:逐 commit 归因(快照 5 个 commit,shallow 本地只可达 1 个);GitHub 真实渲染与真实 API(无 token、无写操作);assign 调用的真实 wire body(本机 GH_DEBUG 不打印请求体,改用 gh 自带 --help + 仓库内 6 处既有先例佐证);整份 229 个测试的文件(只跑了 diff 触及的两个 -t 定向门禁);Windows/macOS;线上约 100 个存量 tracking issue(改由 base 脚本自己生成语料来模拟迁移)。

Scope

The diff touches exactly two files (git diff --numstat HEAD^1..HEAD): .github/scripts/upsert-deferred-issue.sh +126/−8 and scripts/tests/qwen-autofix-workflow.test.js +309/−6. No workflow YAML, no dependency or lockfile change — so the base arm reuses the already-installed root node_modules and the A/B is a pure code comparison with no dependency confound.

Central claim (the behaviour the PR exists to change): on the create path only, fetch the PR once and make the tracking issue self-describing — title base: <PR title>, body naming PR number/title/author plus cc @author, every rc: bullet deep-linked — assign by a separate best-effort call, never retry the create, and on any context failure degrade to the bare form with a warning while still persisting.

Secondary claims (both verified): (a) the rv:/ic: deep-link exemption prevents a one-time duplicate wave over existing tracking issues; (b) the marker-less title fallback adopts both the bare and the enriched form, colon-guarded so PR #5 never prefix-adopts PR #50's issue.

Out of scope and not covered: everything listed in Not covered below.

Central claim — A/B table

Both arms run the real script under the production contract lifted from .github/workflows/qwen-autofix.yml:6577 (a clean env -i child carrying only PATH/WORKDIR/PR/REPO/AUTOFIX_BOT), against a recording gh stub that logs exact argv NUL-delimited and emulates --jq and --paginate the way real gh does. Oracle per cell is the recorded argv plus stdout. Witness: 01-ab-head-vs-base-cells.png.

cell oracle HEAD BASE (control)
A1 happy create -f title= argv …PR #5: Some PR title …PR #5 (bare)
A1 cc @someone. in -f body= present absent
A1 #discussion_r7 in body present absent
A1 POST …/issues/77/assignees count 1 0
A1 GET …/pulls/5 count 1 0
A2 fetch exit 1 + stderr title / warning bare + could not fetch PR #5 context (;;error;;…) bare, no warning exists
A3 fetch exit 0, 200 text/html warning bare + the call exited 0 but returned no usable PR object n/a (no fetch)
A4 assign rejected create POST count exactly 1 n/a
A5 author is the bot assign / cc 0 / none n/a
A6 login a@b, not a login, 40 chars assign / cc 0 / none n/a
A7 append path GET …/pulls/ count 0 0
A9 marker-stripped enriched title adoption appends to #46, no fork forks a second issue
all arms findings persisted yes yes

Result: 80/80 assertions (ab-suite.mjs, logs-ab.txt). Base-arm reds are encoded as expectations, so they count as passes. A9 is the sharpest cell: the base lookup only tests == $t, so given the enriched title this PR now writes, base forks a duplicate tracking issue — the outcome the file ranks worst.

Two facts established without A/B, because they are deterministic properties of tools rather than of this diff:

  • gh api --help (gh's own manifest) documents key[]=value as the array form and -f/--raw-field as a static string parameter — the @-filename magic belongs to -F. So -f "assignees[]=…" is the correct wire form, and a PR title beginning with @ cannot be read as a filename. Corroborated by 6 files in this repo already shipping -f "labels[]=…" against the sibling array endpoint, including this script's own create call, which already relies on gh's auto-POST (no -X POST) in production.
  • Auto-POST is proven in situ by the same create call having minted the ~100 live tracking issues.

Corrections to the description and to the new test comments

These are corrections to text, not requests to change code.

  1. "this case measures ~4.8s and times out" — on this CI container the base arm of that same case already measures 37,606 ms and head 53,868 ms. The 4.8 s figure is a quiet-machine number. The advice to pass --config is correct, and more load-bearing than stated: against vitest's 5 s default the case fails on any shared runner by roughly an order of magnitude, not marginally.
  2. "perSource only pins [the deep-link exemption] for rv" — the M1 mutant (widen the suffix to every source) was killed at scripts/tests/qwen-autofix-workflow.test.js:14769 by perSource's own pre-existing expect(perSource.calls).not.toContain('?: dup'), which fires before the new icTracked case in source order. So perSource does pin the ic side as well, and icTracked is redundant defence rather than the sole witness — a correct thing to ship, but the comment buys coverage confidence that was already paid for. Caveat: vitest aborts the it() at the first failing assertion, so this attribution is a lower bound, not a complete census of which assertions would catch it.
  3. "restoring that retry shape must red the single-create assertion below" — it does not. See Finding 1.

Findings

1. create is never retried is unpinned, and a test comment claims otherwise (Suggestion)

The script's central durability rule is that POST /repos/{owner}/{repo}/issues is issued once and never retried, because the failures that would reach a retry are the ambiguous ones and a re-POST mints a second tracking issue carrying the same marker that the next round's newest-first lookup orphans forever. The code implements this correctly (A4: exactly 1 create POST).

Mutant M8 restores a single retry-on-failure around the create call. It survived the PR's suite (1 passed / 0 failed), while the positive control and 11 other mutants were killed. The mutation is behaviourally live, not a no-op edit:

head M8 mutant
POST repos/o/r/issues with create failing 1 2
second POST's marker + title identical to the first yes
stdout byte-identical byte-identical
exit status 0 0

Because stdout is identical, no output assertion can see it — only the recorded call log can. The suite's single create-count assertion lives in the assignFailed case, whose create succeeds, so a retry-on-create-failure never reaches it. Reproduce:

cd /__w/qwen-code/qwen-code && node tmp/pr11080-verify-20260906-071334/m8-adjudicate.mjs

Witness: 04-m8-survivor-1-vs-2-create-posts.png. Classification: coverage gap — the behaviour is right, nothing asserts it. Per AGENTS.md a missing test for correct code is a Suggestion, not a Critical. It is worth raising anyway because the un-pinned property guards the outcome this very file ranks worst.

The fixture that would pin it (not applied — advisory only)

The existing writeFail case already sets writeFail: true; it only lacks the create-count assertion that assignFailed applies to its own successful create:

// in the writeFail case
expect(writeFail.calls.split('api repos/o/r/issues -f title=')).toHaveLength(2);

I did not apply and re-run this, so I am not claiming it is measured — the harness above proves the mutant is live and that the current suite is blind to it, which is the part that needed evidence.

2. The file's heaviest case now spends 60% of its 90 s budget (Suggestion)

The PR adds 14 runUpsert subprocess invocations (60 → 74) to the single heaviest it() in a 16 k-line file. Measured on this shared runner, with the same command both arms:

arm case duration budget margin
base (HEAD^1 test + script) 37,606 ms 90,000 ms 2.39×
head (as shipped) 53,868 ms 90,000 ms 1.67×
head, second independent run (matrix control) 61,570 ms 90,000 ms 1.46×

Δ = +16.3 s (+43%), ≈1.16 s per added subprocess. The budget figure is the PR's own claim and is correct: scripts/tests/vitest.config.ts:50 sets testTimeout: … || 90_000. The PR also adds a comment noting that a 30 s cap on this exact test was removed by #10870 after it timed out on contended release runners, citing runs 33676423730 / 33683912557. This change consumes 31% of the headroom that remained after that incident.

This is a flake-risk observation, not a correctness defect, and I want to bound it precisely: the case passed on every run here, and the skill's own note applies — a shared, loaded runner is the regime where such a test passes, so I cannot reproduce a timeout by repetition. What I can say is that the margin is now 1.46–1.67× on hardware representative of the lane, where the same case was 2.39× before. Reproduce:

cd /__w/qwen-code/qwen-code && time npx vitest run --config ./scripts/tests/vitest.config.ts \
  scripts/tests/qwen-autofix-workflow.test.js \
  -t 'upserts deferred findings into a per-PR issue that survives the merge'

3. Body prose claims something about rc: items that a rv:/ic:-only batch does not have (Nit)

"Each rc: item links back to its original review comment." is emitted unconditionally at creation. For a batch containing only rv: or only ic: items — which produce no rc: bullet at all — the issue body still asserts a property of items it does not contain. Measured: rv-only and ic-only batches both persist, both correctly omit the deep link, and both carry the sentence. Cosmetic only; no dedupe or rendering consequence.

cd /__w/qwen-code/qwen-code && node tmp/pr11080-verify-20260906-071334/obs.mjs   # O1

4. A fetch that exits 0 with stderr content discards that stderr (Nit)

The gate's second half overwrites PR_CTX_REASON unconditionally when PR_FETCH_OK == 1, so if gh wrote anything diagnostic to stderr while still exiting 0, the captured reason is replaced by the generic the call exited 0 but returned no usable PR object. Measured: with the stub emitting gh: a diagnostic that would have named the proxy on stderr and an HTML body, the warning does not contain that text. This is a small observability loss in a change whose purpose is observability; it is strictly better than base, which had no warning at all.

cd /__w/qwen-code/qwen-code && node tmp/pr11080-verify-20260906-071334/obs.mjs   # O2

Consequences I tested that do not hold

Bounding the above matters more than escalating it, so these are the scarier readings I tried and disproved:

  • No injection through the contributor-controlled PR title. The title is fully fork-author-controlled and republished under the bot identity. I swept 111 cut positions across five hostile token classes (@victimuser, &#64;victim, &commat;victim, &lt;!\-\-victim-->, and a mix), asserting the body carries no live mention, no live @-entity spelling, and no live &lt;!\-\-. Zero survived. Three cut positions leave a trailing bare @ where the .[0:80] slice falls between @ and its ZWSP; all three are inert because the closing ") follows, and @") is not a parseable mention. The sharpest case is a 79-CJK-char title putting @ at codepoint 80: the body copy ends in a bare @ and no part of victim survives. The shipped order is escape-then-slice, which is the safe one — slice-then-escape would have left a live mention at exactly these positions.
  • No cross-PR adoption collision. Driving hostile titles (Deferred review findings from PR #50, 0: Deferred review findings from PR #50, :, :::) into PR TypeError in Authentication Selection Interface #5's created title, PR refactor(cli): update OpenAI API key prompt with Bailian URL #50's marker-less lookup never adopts PR TypeError in Authentication Selection Interface #5's issue and always creates its own, while PR TypeError in Authentication Selection Interface #5 still adopts its own — including after a retitle and past the 80-codepoint cap, since adoption is prefix-based on the base title.
  • No new public cross-reference target. The accepted-tradeoff list does not mention timeline effects, so I checked the sibling: base already carried the #N shorthand in the body, and head carries the same single #5 plus an absolute URL to the same PR. The referenced-target set does not grow. (I could not exercise GitHub's real timeline API — see Not covered.)
  • No duplicate wave over the ~100 existing issues. The corpus was not hand-typed: the base script authored it, then head replayed the same rc/rv/ic findings against that real base-authored body and re-published 0 items. The widening mutant re-published exactly 2 — the rv and ic lines — while rc stayed suppressed by its id anchor. Witness: 03-duplicate-wave-head-0-vs-mutant-2.png.
  • No GH_ERR cross-contamination. Three sequential gh calls each reset the sink; the assign warning names the assign reason, and when both fetch and create fail, the two warnings name their own distinct reasons.
  • No codepoint damage. A CJK title slices to exactly 80 codepoints / 240 bytes with no U+FFFD; a 100-emoji title slices to 80 codepoints with no lone surrogate and round-trips as valid UTF-8. The comment's rationale (a bash byte slice would cut a UTF-8 sequence) is borne out.
  • Degenerate payloads never lose findings. Array, empty object, empty string, HTML, numeric title, null title, null user, null login, nested-object title, embedded \u0000: all 10 persist, all exit 0, all issue exactly one create, and the warning fires exactly when nothing usable came back.

Mutation matrix

Witness: 02-mutation-matrix-12-of-13-killed.png. Oracle is the PR's own suite via vitest's JSON reporter; the unmutated control is green (1 passed / 0 failed) and the positive control is killed, so the harness demonstrably can make this suite fail.

mutant claim under test verdict first failing assertion (qwen-autofix-workflow.test.js)
PC rename the dedupe marker positive control KILLED :14234 marker present in the create call
M1 widen deep-link suffix to all sources rv/ic exemption KILLED :14769 perSource (see Correction 2)
M2 drop the colon guard in the lookup enriched adoption KILLED :15064 enriched-title adoption
M3 drop the login-charset guard malformed .user.login KILLED :14327 badLogin (space)
M4 gate on call status only second half of the warning gate KILLED :14295 prUnusableBody warning
M5 delete the context warning observability KILLED :14272 prFetchFailed warning
M6 raw ${GH_ERR} instead of gh_reason() :: neutralisation KILLED :14272 ;;error;; vs ::error::
M7 hoist the fetch above the create/append branch creation-only context KILLED :14455 append issues no pulls/
M8 retry the create on failure never retried SURVIVED none — suite stayed green
M9 drop -n "${NUM}" from the assign guard no assign after failed create KILLED :14516 writeFail sees no assignees
M10 title uses the escaped copy split title/body derivation KILLED :14387 titledTitle exact match
M11 widen {1,39} to {1,} 40-char login KILLED :14345 longLogin
M12 admit @ into the charset class a@b login KILLED :14336 atLogin

12/13 killed, 1 survived, 0 build failures. Every declared mutation claim in the new test comments held except M8's (Correction 3 / Finding 1). Vacuity of the central new test is proven independently: head tests against the base script go red with AssertionError: expected '…' to contain 'api repos/o/r/pulls/5' at :14240 — an assertion mismatch naming expected-vs-actual, not a crash. Only one assertion can surface because vitest aborts the it() at the first failure.

Not covered

  • Per-commit attribution. The snapshot lists 5 commits; the depth-2 merge-ref checkout makes 1 reachable (git rev-list HEAD^1..HEAD^2). A bare git rev-list --count returns a plausible 1 at the shallow boundary rather than erroring, so this was checked against the snapshot, not assumed. I verified the aggregate HEAD^1..HEAD diff only; the four earlier commits' individual claims were not separately exercised.
  • The real GitHub renderer and API. No token and no writes in this job. The design premise that issue titles are stored and rendered as plain text — no markdown pass, no mention filter is what justifies leaving the title unescaped while escaping the body; I verified the script implements that split (title raw, body neutralised, asserted per call so neither surface can be credited with the other's rendering) but I could not observe GitHub actually render a title. Every escaping assertion is against recorded argv, not rendered HTML.
  • The real gh wire body for the assign call. GH_DEBUG=1 does not print request bodies in this build, so the body was verified from gh api --help (the tool's own manifest) plus 6 in-repo precedent files, not from a captured payload. One network probe was made — a POST to a deliberately nonexistent repo path with an invalid token, which returned 401 Bad credentials and had no side effect.
  • The full 229-test file. I ran two targeted -t gates: the upsert block (which contains every new behavioural case) and posts a human-handoff marker… (which contains the diff's other changed assertion, scriptEscapeSites toHaveLength(1)2; green in 20.9 s). The remaining 227 tests were skipped by the filter and not run.
  • Repo-wide gates. No npm run test:scripts, no eslint, no typecheck: the diff touches no TypeScript source, and the two files it touches are covered by the targeted gates above.
  • shellcheck's repo wrapper is not a blocking gate. node scripts/lint.js --shellcheck exits 0 despite hundreds of findings across the repo, because its pipeline ends in sed, so the status is sed's. A clean result there would be weak evidence, so I ran shellcheck 0.11.0 (the repo's pinned version) directly with the repo's exact flags on head and base: head 10 findings (SC2154×4, SC2312×6), base 9 (SC2154×4, SC2312×5). The delta is +1 SC2312 note at the new assign warning's $(gh_reason) — the identical idiom the file already uses twice at base. No new finding class, no error-severity finding. The gate's liveness was proven by planting SC2034 and SC2164 violations and confirming both are reported.
  • yamllint could not be installed (pip3: Permission denied). Immaterial: the diff changes no YAML.
  • Windows / macOS. The script is bash + jq + gh; the repo's own config excludes bash-driven workflow suites on Windows. Only Linux was exercised.
  • The ~100 live tracking issues. No network/token, so the migration was simulated by having the base script author the corpus rather than by reading real issues. This reproduces the shape of the migration (base-authored bytes, replayed through head), not the real issues' contents.
  • No calibration against a real emitted artifact. This is a script PR, not a workflow-step PR: there is no posted comment or uploaded file whose bytes a replay could be calibrated against, so nothing here claims calibration. What substitutes for it is that every cell drives the shipped script itself, under the invocation contract copied from the workflow, with the base arm as a live control.
  • jq version. This container has jq 1.6; I did not establish which version the production runners use. All jq programs exercised here are 1.6-compatible, but a 1.7-only behaviour difference would not have shown up.

Methodology

Everything ran in the CI verify container (node:22-bookworm, node v22.23.2, bash 5.2.15, jq 1.6) against refs/pull/11080/merge at 68b2f87c. The unit under test is the shipped bash script, executed for real — never stubbed — under the env -i child contract transcribed from .github/workflows/qwen-autofix.yml:6577. The only fake is gh: a recording stub (stub-gh.sh) that appends each invocation's exact argv NUL-delimited and emulates --jq (client-side filtering, which the script depends on for --jq '.number' and --jq '.body // ""') and --paginate (one JSON array per page). Assertions read that argv, so they judge the wire rather than the script's own narration. The base arm is git show HEAD^1:.github/scripts/upsert-deferred-issue.sh; the diff changes no package.json or lockfile and no internal workspace package is on the code path, so reusing the installed root node_modules is a clean control and no realpath check was needed. Mutants are single-point string edits with an exactly-one-occurrence precondition and a bash -n check before use; each swaps the working-tree script, runs vitest under the JSON reporter, and restores in a finally, with the final sha256 asserted equal to the recorded pristine value (df9566be…) and git status --porcelain confirmed empty. Harnesses: ab-suite.mjs (80), census.mjs (20), hostile.mjs (855), obs.mjs (22), m8-adjudicate.mjs (8), gates.mjs (35) — 1020 assertions total, all in this directory with their raw logs (logs-*.txt), rerunnable by a maintainer. Two harness bugs were found and fixed during the round and are recorded here because they shaped early output: a brace-bearing ${VAR:-{…}} default in the stub silently appended a stray } to every JSON payload, and a --paginate emulation that double-wrapped the page array; both were stub faults, proven so by isolation before any conclusion was drawn from them.

A third was caught only by its own positive control and is worth naming, because left undetected it would have fabricated a result rather than crashing: the first mutation-matrix run detected "green" with a /Tests 1 passed/ regex, but vitest wraps that summary in ANSI escapes even when redirected, so the regex never matched and the unmutated control reported NOT GREEN. Had the control not been run first, all 13 mutants would have been reported KILLED — a clean 13/13 that meant nothing. The run was discarded, the tree restored and hash-verified, and the oracle replaced with vitest's JSON reporter (testResults[].assertionResults[]) before the matrix was repeated. This is also why the matrix quotes a positive control at all: it is the only thing that distinguishes "the suite covers this" from "my harness never observed the suite".

Flakiness gate log

rounds=5 files=1 skipped=0
file scripts/tests/qwen-autofix-workflow.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/qwen-autofix-workflow.test.js


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  scripts/tests/qwen-autofix-workflow.test.js: PPP

verdict: timeout
summary: only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed

--- per-invocation detail (full copy in the artifact) ---
round 1 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)
round 2 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)
round 3 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)

Evidence images

01-ab-head-vs-base-cells

02-mutation-matrix-12-of-13-killed

03-duplicate-wave-head-0-vs-mutant-2

04-m8-survivor-1-vs-2-create-posts

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

Qwen Code · sandboxed verification

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind main, so it merged current main in via update-branch and will retry on the next scan. A stale base (a dependency or symbol main already changed) can fail the build without being the fix's fault; if it still fails once current, it hands off to a human.

What I found before stopping:

Round blocked: required checks cannot run — this runner's node_modules is incomplete

Both prepared fixes (R3-1 gate pin, R3-3 mega-test split) are fully implemented and verified by every check this sandbox can run — but npm run build, npm run typecheck, and npm run lint all fail on this runner for missing npm packages, before evaluating anything, on every tree state including unmodified HEAD. The skill forbids committing while a required runnable check is failing, and the .husky pre-commit hook fails for the same infrastructure reason (its eslint leg cannot load the config), so the commit itself could not be created. The prepared change was reverted; the tree is clean at ba2b985123. An exact redo recipe is included below.

The blocker (infrastructure, not the change)

Exact commands and their real outputs on this runner (each run multiple times, including on the untouched HEAD tree):

  • npm run build → exit 1: Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'vite-plugin-dts' imported from .../node_modules/.vite-temp/vite.config.ts... — fails in packages/webui (vite build).
  • npm run typecheck → exit 1: error TS2307: Cannot find module '@qwen-code/web-templates' or its corresponding type declarations (3 errors in packages/cli) — packages/web-templates/dist was never built in this workspace.
  • npm run lint → exit 2: `Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'eslint-plugin-storybook' imported from .../eslint.config
中文说明

🤖 AutoFix 更新了一个过期的 base —— 修复未通过验证,但本 PR 落后于 main,因此已通过 update-branch 合入当前 main,并将在下次扫描时重试。过期的 base(main 已改动的依赖或符号)可能让构建失败而并非修复本身的错;若 base 更新后仍然失败,将移交人工处理。

停止前我了解到的情况:

本轮受阻:必需检查无法运行——本运行器的 node_modules 不完整

本轮已准备好的两项修复(R3-1 门钉断言、R3-3 超大用例拆分)均已完整实现,并通过了这个沙箱能运行的全部检查——但 npm run build、npm run typecheck、npm run lint 在本运行器上因缺少 npm 包而失败,失败发生在评估任何内容之前,且在包括未改动 HEAD 在内的所有树状态下都失败。技能规则禁止在必需可运行检查失败时提交,而 .husky 的 pre-commit 钩子也因同一基础设施原因失败(其 eslint 环节无法加载配置),因此提交本身也无法创建。已准备好的改动已被还原,工作树干净地停在 ba2b985。下方附有精确的重做配方。

阻塞点(基础设施,与本次改动无关)

在本运行器上实际执行的命令及其真实输出(每条都运行了多次,包括在未改动的 HEAD 树上):

  • npm run build → 退出码 1:Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'vite-plugin-dts' imported from .../node_modules/.vite-temp/vite.config.ts... —— 在 packages/webui(vite build)处失败。
  • npm run typecheck → 退出码 1:error TS2307: Cannot find module '@qwen-code/web-templates' or its corresponding type declarations(packages/cli 共 3 处)—— 本工作区的 packages/web-templates/dist 从未构建。
  • npm run lint → 退出码 2:Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'eslint-plugin-storybook' imported from .../eslint.config.js。
  • git commit → 被 .husky pre-commit 钩子阻断:lint-staged 的 eslint --fix 在加载 eslint.config.js 时因同样缺少 eslint-plugin-storybook 而崩溃。

这是运行器问题而非本轮问题的证据:

  • package-lock.json 声明了 node_modules/eslint-plugin-storybook@10.2.0 与 packages/webui/node_modules/vite-plugin-dts@4.5.4,packages/webui/package.json 也声明了这两个包——但 node_modules/.package-lock.json(本作业自己的安装记录,mtime 为今天 05:38)两者都不包含;磁盘上不存在 node_modules/eslint-plugin-storybook 与 packages/webui/node_modules,packages/web-templates/ 下也没有 dist/。本作业的安装静默地产出了一棵不完整的依赖树。
  • 本轮唯一改动的文件 scripts/tests/qwen-autofix-workflow.test.js 不是这三条失败命令中任何一条的输入:它是位于仓库根级、不属于任何 workspace 的 .js 文件(不在任何 vite build / tsc 的输入集合内,且 npm run lint 是 eslint . --ext .ts,.tsx,永远不会匹配它)。三条命令都在工具/配置启动阶段失败,在未改动的 HEAD 上完全相同——该失败不可能由本轮改动引起,也不可能由本轮改动修复。

为什么没有尝试本地修复

本模式受信任的命令集(npm run build、npm run typecheck、npm run lint、聚焦的 vitest、npm run generate:settings-schema)不包含任何安装命令。npm ci 是工作流

Run log: https://github.com/QwenLM/qwen-code/actions/runs/34014105020


🧠 Handled by Qwen Code · model/模型 kimi-k3

…item issue

The tracking issue this script creates is assigned to the PR author right
after creation, and the scheduled ready-for-agent scan filters
`no:assignee` (AUTOFIX_ISSUE_EXCLUDES in qwen-autofix.yml), so an issue
carrying an assignee never re-enters that scan. The body still invited
readers to "apply the ready-for-agent flow" without saying to which issue,
which reads as an invitation to label the tracking issue itself - the one
place where a label-event run cancelled by the per-issue concurrency group
has no scheduled scan left to retry it.

Attach the flow to the per-item issue a human files from this list instead:
that issue is unassigned, so it keeps the scan backstop. Pinned per call in
the upsert case; mutation-verified red when the old parenthetical is
restored.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtpq5ybkn4
@yiliang114

Copy link
Copy Markdown
Collaborator Author

CI attribution for the red Test (ubuntu-latest, Node 22.x): not caused by this PR — the job hit its own 120-minute ceiling on the shared ecs-qwen pool, it did not fail a single assertion.

Evidence (head 3e05ec6401, run 34021870820, job 101455870624):

  • gh pr checks renders it as fail, but the job's own conclusion is cancelled, and so is the run's.
  • Job started_at 08:27:21Zcompleted_at 10:27:54Z = 2h0m33s, against timeout-minutes: 120 that ci.yml:376 routes to the ecs-qwen pool. The step's own cap is 110 (ci.yml:691), so the job ceiling fired first.
  • Step 16 Run tests and generate reports ran 08:48:59Z → cancelled 10:27:35Z. Step 13 Install dependencies alone took 21m13s (08:27:46Z08:48:59Z), leaving ~98 min of the step budget.
  • Step 19 Publish Test Report is failure only because the cancelled step produced no packages/*/junit.xml; steps 17/18/21 are skipped behind it. No failing test name anywhere in the job's step list.
  • Sibling lanes in the same run all passed: Lint & Static 31m24s, Integration Tests (no-AK) 10m10s, web-shell E2E Smoke 11m3s.
  • The same check is red on main right now: run 34027142264 (main, 10:20Z) Test job failure after 73.5 min, run 34022807324 (main, 09:02Z) Test job failure after 63.8 min. ci.yml:369-375 documents the contention this ceiling exists for ("Shared ECS hosts can stretch the normally 25-minute lane beyond an hour").
  • This PR's diff touches only .github/scripts/upsert-deferred-issue.sh and scripts/tests/qwen-autofix-workflow.test.js, which run in the npm run test:scripts phase (ci.yml:760-763) after test:ci:workspaces. Locally the pinned upsert case measures 4.5s (npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js -t 'upserts deferred findings into a per-PR issue that survives the merge'Tests 1 passed | 236 skipped).

Not blind-fixing it. The push of 239efbfc9b supersedes this run anyway, so the fresh run is the retry; if the pool is still contended the same ceiling will bite any PR, and the lever is runner capacity or the 120/110 pair in ci.yml, not this diff.

qwen-autofix/dispatch-pending was pending at the time of writing (review-address dispatched, run 34030226879) — expected, not a failure.

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

Reviewed.

3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R2-6 create-time assignment's issues.assigned webhook self-trigger in qwen-autofix.yml — already reported (comment 3941924819)
  • the single-create-POST witness running only where the create succeeds — already recorded (round 5 deferral, scripts/tests/qwen-autofix-workflow.test.js:14456)
  • the assign warning's captured reason never asserted — already recorded (round 4 deferral, scripts/tests/qwen-autofix-workflow.test.js:14351)

Not explored to full depth (tool budget reached): "agent 6c": running npm run test:scripts -- -t 'upserts deferred findings' to confirm the new assertions pass on this host.

1 Suggestion(s) were drafted inline past the resolved critical posting floor; the CLI moved them into the deferral list below (floor enforcement).

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

  • .github/scripts/upsert-deferred-issue.sh:486 — [review] This comment is the head commit's own rationale for pointing the ready-for-agent flow at the per-item issue, and it argues one state of a gate that covers two. It rests on the tracking…
  • .github/scripts/upsert-deferred-issue.sh:518 — [review] Success line for the durable write sits behind the assign network call
  • .github/scripts/upsert-deferred-issue.sh:492 — [review] Body tells the cc'd PR author to do something only a maintainer can do
  • scripts/tests/qwen-autofix-workflow.test.js:15330 — [review] Added comment states a 30s child timeout the harness does not have
中文说明

已审查。

本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未探索到全部深度(达到工具调用预算):"agent 6c"running npm run test:scripts -- -t 'upserts deferred findings' to confirm the new assertions pass on this host

1 条 Suggestion 在已解析的 critical 发布下限之外被起草为行内评论;CLI 已将其移入下方延后清单(下限强制执行)。

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

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [Lint & Static (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Lint & Static (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@yiliang114

Copy link
Copy Markdown
Collaborator Author

CI attribution — Test (ubuntu-latest, Node 22.x) on head 1b9b671d3f is self-hosted pool contention, not this diff. Re-running the failed jobs; no code change.

Run 34039883783 (Qwen Code CI, head_sha = 1b9b671d3fa6d654185b75efd0c7166fbb3e76b5, attempt 1), overall conclusion cancelled.

Evidence from job 101504642264 (runner github-runner-hk3-27 — self-hosted ECS, despite the ubuntu-latest label):

  • Install was fine. Steps 1–15 all success, including Disk floor gate (self-hosted) and Install dependencies. This is not the install/disk-pressure signature that killed the sibling jobs on feat(channels): add shared multiline instructions field to channel management #11082.
  • The job hit its own budget. Job ran 14:40:27Z → 16:41:22Z = 2h00m55s against timeout-minutes: 120 (the ecs-qwen branch of ci.yml:384). Step Run tests and generate reports ran 15:03:46Z → 16:40:40Z = 97 min against its own timeout-minutes: 110 (ci.yml:699), so the job budget expired first and the step ended with ##[error]The operation was canceled.
  • The runner then killed the suite: Terminate orphan process: pid (246116) (npm run test:ci:workspaces --retry=2), node (vitest), esbuild. That is verbatim the class ci.yml:689-691 already documents — "a hung suite used to burn the whole job budget … cancelled … with the runner terminating orphaned test processes, which reads as a timeout rather than a failure." ci.yml:693-698 also sets the expectation: ~32 min at six forks, "the pool now runs three, which roughly doubles the test phase", entering the step "at around minute 18" — it entered at minute 23 here, leaving 97 min of job budget, all of which it consumed.
  • What was actually stuck: at cancellation the suite was in packages/core on createContentGenerator / validateModelConfig, failing with ENOENT: no such file or directory, open '/var/tmp/qwen-ci-662Xln/…/ssr/<hash>' (vitest SSR transform cache on runner-local /var/tmp) and one case taking 6m. 186 log lines match disk/ENOSPC/OOM/killed signals.
  • The diff cannot be responsible. grep -c "qwen-autofix-workflow" over the full 3.3 MB job log returns 0 — this PR's own modified test file never appears, i.e. the suite never reached scripts/tests/. The changed files are .github/scripts/upsert-deferred-issue.sh (bash, executed by the autofix workflow, not by this job) and scripts/tests/qwen-autofix-workflow.test.js. No runtime code, no test config, no dependency change.
  • Corroboration: Lint & Static, Integration Tests (no-AK) and both Desktop Shell lanes are green on this same head, and 5 of the last 12 Qwen Code CI runs on main in the same 10:20–15:46Z window were also cancelled — the pool was contended that afternoon.

Separately, web-shell E2E Smoke (job 101521704200) failed at step Run web-shell browser smoke, starting 16:41:26Z — four seconds after Test was killed. Different lane, different signature, and this PR touches no web-shell code. Included in the re-run.

wenshao
wenshao previously approved these changes Sep 6, 2026
@wenshao
wenshao enabled auto-merge September 6, 2026 17:13

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

2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • D7-2 the added comment's 30s child-timeout figure — already recorded (round 6 deferral, scripts/tests/qwen-autofix-workflow.test.js:15330)
  • D7-3 the assign call's issues.assigned self-trigger in qwen-autofix.yml — already reported (comment 3941924819, R2-6)

Not reviewed: the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": executing scripts/tests/qwen-autofix-workflow.test.js under vitest — the review worktree has no node_modules , so the ~14 new runUpsert cases were checked …; "agent reverse-audit (round 1)": enumerating runs-on for each lane that runs npm run test:scripts ( ci.yml:767 , release.yml:701 , qwen-triage.yml:3594 ) — I established the new script co…; "agent 2": verifying against the live GitHub API whether an issue title stores C0 control bytes and U+202E or strips them server-side (no authenticated network probe avail…; "agent 4": running scripts/tests/qwen-autofix-workflow.test.js under vitest to measure the heaviest case's total idle runtime and its real margin under the 90 s testTim….

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

  • scripts/tests/qwen-autofix-workflow.test.js:14527 — [review] No fixture witnesses the codepoint-safety of the jq .[0:80] title cap
中文说明

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

本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"executing scripts/tests/qwen-autofix-workflow.test.js under vitest — the review worktree has no node_modules , so the ~14 new runUpsert cases were checked …"agent reverse-audit (round 1)"enumerating runs-on for each lane that runs npm run test:scripts ( ci.yml:767 , release.yml:701 , qwen-triage.yml:3594 ) — I established the new script co…"agent 2"verifying against the live GitHub API whether an issue title stores C0 control bytes and U+202E or strips them server-side (no authenticated network probe avail…"agent 4"running scripts/tests/qwen-autofix-workflow.test.js under vitest to measure the heaviest case's total idle runtime and its real margin under the 90 s testTim…

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

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

…he assignment comments

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round summary — PR #11080

Commit 439cd8e872fix(ci): pin the never-retried deferred-findings create and correct the assignment comments (2 files, +31/−15).

Addressed

1. F1 — the never-retried create is now pinned (ic:5557397302 Finding 1; ic:5557642971 F1; ic:5558163896 F1)

Both verifiers measured the same survivor: a mutant that re-POSTs the create once on failure stays green, because the suite's only create-count assertion lives in assignFailed, whose create succeeds — a retry-on-create-failure branch is never entered there. Fixed two ways:

  • scripts/tests/qwen-autofix-workflow.test.js (writeFail case, the one that does fail the create): added the create-count assertion using the file's existing split-on-the-call-head idiom — writeFail.calls.split('api repos/o/r/issues -f title=') must have length 2. Only the call log can see a retry here: with the create failing, both arms print the identical LOST warning, so no output assertion can distinguish them.
  • Corrected the assignFailed comment, which claimed "restoring that retry shape must red the single-create assertion below". It cannot — the comment now says why (this case's create succeeds, so the retry branch never runs) and where the pin actually lives.

Mutation probe (the round's witness): temporarily rewrote the create call to retry once on failure → the suite goes red with AssertionError: expected [ …(3) ] to have a length of 2 but got 3 at the new assertion; restored the script → green again. The M8/M11 survivor both verifiers measured is now killed.

2. F2 — the two assignment comments now state the real semantics (ic:5557642971 F2; ic:5558163896 F2)

The comment above the separate assign call claimed create-time assignees are "silently dropped for users without push access" with "the create always returned 201 unassigned", and that "Failure only warns". The verifier measured against the live API and found both backwards:

  • POST /repos/{owner}/{repo}/issues with a non-assignable assignees[] returns 422 Validation Failed — the whole create fails (assignees octocat cannot be assigned to this issue).
  • The separate POST …/issues/{n}/assignees for a non-assignable author does not fail — exit 0, no stderr, assignees stays [], so the could not assign warning never fires in that case.

Rewrote the comment at .github/scripts/upsert-deferred-issue.sh:507-515 to say exactly that. The correction argues for this PR's separate-call design more strongly than the old text did: create-time assignment would lose the findings entirely to a 422 for exactly the external contributors the branch exists for. Comment-only change; no code touched.

3. Two overstated wordings softened (ic:5558163896, "Two wordings I'd soften")

  • "an auto-loading beacon" → "an attacker-chosen image (camo-proxied, so not a per-reader beacon)" — GitHub rewrites ![](…) to camo.githubusercontent.com, so it is not a per-reader beacon; the real harm is an attacker-chosen image rendered inside bot-authored text. (.github/scripts/upsert-deferred-issue.sh:414)
  • "an unlabeled fold" → "GitHub's generic Details fold" — GitHub supplies its own <summary>Details</summary>; the fold is labelled, just uninformatively. Fixed in the script (:417) and in the mirrored test comment (titled case).

Declined (issue-level findings — no threads to reply on, so recorded here)

Sandbox Finding 2 — the file's heaviest case spends ~60% of its 90s budget (ic:5557397302)

Same shape as inline R3-3, already declined on-thread in two earlier rounds with reasons, and the decline stands: the quantified timeout-margin claim was withdrawn by the reviewer itself, the case passed on every measured run, and splitting the mega-case is a test-architecture refactor of a 25k-line file this CI-plumbing PR does not own — it belongs in its own PR with its own before/after budget measurement. The deterministic gate re-runs the full suite on every round; a real timeout would surface there, not in prose.

Sandbox Nit 3 — "Each rc: item links back to its original review comment." is unconditional (ic:5557397302)

The sentence quantifies over the rc: items present; for an rv-only/ic-only batch it is vacuously true, and the verifier records no dedupe or rendering consequence. Making it conditional adds a script branch for pure cosmetics — declined under the budget warning's decline-nonessentials rule.

Sandbox Nit 4 — an exit-0 fetch discards stderr gh wrote (ic:5557397302)

gh api exiting 0 with diagnostics on stderr is a rare edge; the current reason ("the call exited 0 but returned no usable PR object") is accurate for the observed case, the verifier itself rates this strictly-better-than-base, and a fix would need a script branch plus a stub extension plus a new fixture for a Nit. Declined as not worth the diff growth this round.

Left open by design (no action this round)

Failed checks

  • web-shell E2E Smoke (ubuntu-latest Node 22.x)CANCELLED, infrastructure, not this diff. The attribution in ic:5560791893 shows the sibling Test job hit its own 120-minute ceiling on the contended self-hosted ecs-qwen pool (the runner killed vitest mid-suite before it ever reached scripts/tests/ — this PR's test file appears 0 times in the 3.3 MB job log), and the smoke lane started four seconds after that kill. This PR touches no web-shell code. No code change; the re-run was already triggered in that comment.

Conflict notes

--conflict false; no merge was performed or needed.

Verification

Commands actually run (this harness kills any command at 120 s, which shaped what could run locally — see the last item):

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js -t 'upserts deferred findings into a per-PR issue that survives the merge'1 passed | 236 skipped (237), case 26.8 s, on the final tree.
  • Mutation probe: create call rewritten to retry once on failure (the M8/M11 shape both verifiers used), same command — 1 failed, AssertionError: expected [ …(3) ] to have a length of 2 but got 3 at qwen-autofix-workflow.test.js:14647 (the new writeFail assertion). Script restored, re-run — 1 passed | 236 skipped. Probe result: the previously surviving mutant is killed.
  • npx vitest run … -t 'neutralizes both workflow-command syntaxes at every untrusted echo'passed (reads the script source).
  • npx vitest run … -t 'posts a human-handoff marker when review addressing reaches a terminal handoff'passed (contains the scriptEscapeSites census over the script source).
  • npx vitest run … -t 'keeps the round status comment live with a heartbeat and a job deep link'passed (references the script in its fixture list).
  • npx prettier --check scripts/tests/qwen-autofix-workflow.test.jsexit 0, "All matched files use Prettier code style!".
  • npx eslint scripts/tests/qwen-autofix-workflow.test.jsexit 0, no findings.
  • bash -n .github/scripts/upsert-deferred-issue.shexit 0 (also after the probe restore).
  • node scripts/lint.js --shellcheck — shellcheck is not installed on this runner (xargs: shellcheck: No such file or directory); the wrapper exits 0 regardless and CI's dedicated lane runs the pinned 0.11.0. The script diff is comment-only, so the finding set cannot change.

Not run locally, with the reason: the full npm run test:scripts suite / whole 237-test file (~140 s for this file alone here), npm run build, npm run typecheck, and whole-repo npm run lint all exceed this harness's 120 s-per-command ceiling — every attempt was killed at the tool timeout, and the sandbox reaps detached background processes. The diff touches no TypeScript, no package manifest, and no build input, so those gates re-run byte-identical inputs against a state that was green at checkout (the runner's own pre-flight npm ci + npm run build, and the green Lint & Static lane on this head per the CI attribution in the feedback). The deterministic post-push gate re-runs all of them.

中文说明

Autofix 本轮总结 —— PR #11080

提交 439cd8e872 —— fix(ci): pin the never-retried deferred-findings create and correct the assignment comments(2 个文件,+31/−15)。

已处理

1. F1 —— "create 永不重试"现在有了钉子(ic:5557397302 发现 1;ic:5557642971 F1;ic:5558163896 F1)

两位验证者测到了同一个存活变异体:在 create 失败后重发一次 POST 的变异体能保持全绿,因为套件里唯一的 create 次数断言位于 assignFailed 用例,而那里 create 是成功的——"失败后重试"的分支根本进不去。从两处修复:

  • scripts/tests/qwen-autofix-workflow.test.jswriteFail 用例,真正让 create 失败的那个):按文件既有的"按调用头切分"写法新增 create 次数断言——writeFail.calls.split('api repos/o/r/issues -f title=') 长度必须为 2。这里只有调用日志能看到重试:create 失败时两种形态打印的 LOST 告警完全相同,任何输出断言都无法区分。
  • 更正了 assignFailed 的注释——它原先声称"恢复重试形态必须让下面的单次创建断言变红"。这做不到;注释现在说明了原因(本用例的 create 成功,重试分支不会运行)以及真正的钉子在哪里。

变异探针(本轮见证):临时把 create 调用改成失败后重试一次 → 套件变红,报 AssertionError: expected [ …(3) ] to have a length of 2 but got 3,位置正是新断言;恢复脚本 → 重新变绿。两位验证者测到的 M8/M11 存活变异体现已被杀死。

2. F2 —— 两处 assignment 注释改为真实语义(ic:5557642971 F2;ic:5558163896 F2)

独立 assign 调用上方的注释原先声称:create 时携带的 assignees 对无 push 权限的用户会被"静默丢弃","create 总是返回 201 unassigned",且"失败只会告警"。验证者对真实 API 实测发现两条都说反了:

  • POST /repos/{owner}/{repo}/issues 带不可 assign 的 assignees[] 会返回 422 Validation Failed——整个 create 失败assignees octocat cannot be assigned to this issue)。
  • 独立的 POST …/issues/{n}/assignees 对不可 assign 的作者根本不会失败——退出码 0、无 stderr、assignees 保持 [],因此 could not assign 告警在该情形下永不触发。

已把 .github/scripts/upsert-deferred-issue.sh:507-515 的注释改写为上述事实。订正后的理由比原文更强地支持本 PR 的分离调用设计:对这个分支所要服务的外部贡献者来说,create 时指派会让 findings 因 422 整体丢失。仅注释改动,未触碰代码。

3. 两处夸大的措辞已软化(ic:5558163896,"两处建议软化的措辞")

  • "an auto-loading beacon"(自动加载的信标)→ "an attacker-chosen image (camo-proxied, so not a per-reader beacon)"——GitHub 会把 ![](…) 重写到 camo.githubusercontent.com,所以它不是逐读者信标;真实的危害是攻击者选定的图片渲染在 bot 署名的正文里。(.github/scripts/upsert-deferred-issue.sh:414
  • "an unlabeled fold"(无标签折叠)→ "GitHub's generic Details fold"——GitHub 会自己补 <summary>Details</summary>;折叠有标签,只是没有信息量。脚本(:417)与测试里镜像的注释(titled 用例)同步修正。

已拒绝(issue 级发现——没有线程可回复,故记录在此)

沙箱发现 2 —— 文件最重单用例占用 90 秒预算的约 60%(ic:5557397302)

与行内 R3-3 同形,已在之前两轮的线程里带理由拒绝,拒绝仍然成立:量化的超时余量结论已被评审者自己撤回,该用例在所有实测运行中都通过,而拆分为巨型用例是对这个 CI 管道 PR 并不拥有的 2.5 万行文件的测试架构重构——应当单独开 PR 并自带前后预算测量。确定性门禁每轮都会重跑完整套件;真发生超时会在那里暴露,而不是在文字里。

沙箱 Nit 3 —— "Each rc: item links back to its original review comment." 无条件出现(ic:5557397302)

这句话是对当前 rc: 条目的全称量化;对仅含 rv/ic 的批次它是空虚为真的,且验证者记录它没有去重或渲染后果。把它改成条件式只为观感增加一个脚本分支——按预算告警的"拒绝非必要项"规则拒绝。

沙箱 Nit 4 —— 退出码 0 的 fetch 丢弃了 gh 写入的 stderr(ic:5557397302)

gh api 退出码为 0 却往 stderr 写诊断是罕见边角;当前措辞("the call exited 0 but returned no usable PR object")对已观测情形是准确的,验证者自己也评定其严格优于 base,而修复需要为一个 Nit 增加脚本分支、桩扩展和新 fixture。以本轮不值得增加 diff 为由拒绝。

刻意保持开放(本轮不动)

失败的检查

  • web-shell E2E Smoke (ubuntu-latest Node 22.x) —— CANCELLED,基础设施原因,与本 diff 无关。 ic:5560791893 的归因显示:同跑的 Test 作业在争抢中的自建 ecs-qwen 机器池上撞到自身 120 分钟上限(runner 在套件跑到 scripts/tests/ 之前就杀掉了 vitest——本 PR 的测试文件在 3.3 MB 作业日志里出现 0 次),smoke 通道在被杀 4 秒后才启动。本 PR 不触碰任何 web-shell 代码。不改代码;该评论里已触发重跑。

冲突说明

--conflict false;本轮未执行也不需要合并。

验证

实际运行过的命令(本执行环境会在 120 秒处杀掉任何命令,这决定了本地能跑什么——见最后一条):

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js -t 'upserts deferred findings into a per-PR issue that survives the merge' —— 1 通过 | 236 跳过(237),用例 26.8 秒,最终代码树上。
  • 变异探针:把 create 调用改为失败后重试一次(两位验证者使用的 M8/M11 形态),同一命令 —— 1 失败AssertionError: expected [ …(3) ] to have a length of 2 but got 3,位于 qwen-autofix-workflow.test.js:14647(新增的 writeFail 断言)。恢复脚本后重跑 —— 1 通过 | 236 跳过。探针结论:原先存活的变异体已被杀死。
  • npx vitest run … -t 'neutralizes both workflow-command syntaxes at every untrusted echo' —— 通过(读取脚本源码)。
  • npx vitest run … -t 'posts a human-handoff marker when review addressing reaches a terminal handoff' —— 通过(包含对脚本源码的 scriptEscapeSites 统计)。
  • npx vitest run … -t 'keeps the round status comment live with a heartbeat and a job deep link' —— 通过(fixture 列表引用该脚本)。
  • npx prettier --check scripts/tests/qwen-autofix-workflow.test.js —— 退出码 0,"All matched files use Prettier code style!"。
  • npx eslint scripts/tests/qwen-autofix-workflow.test.js —— 退出码 0,无发现。
  • bash -n .github/scripts/upsert-deferred-issue.sh —— 退出码 0(探针恢复后同样通过)。
  • node scripts/lint.js --shellcheck —— 本机未安装 shellcheck(xargs: shellcheck: No such file or directory);该包装器无论如何都退出 0,CI 的独立通道使用钉定的 0.11.0 运行。脚本 diff 仅注释,发现集合不可能变化。

未在本地运行及原因:完整 npm run test:scripts 套件/整个 237 用例文件(仅该文件在本机约 140 秒)、npm run buildnpm run typecheck、全仓 npm run lint 都超过本环境的 120 秒单命令上限——每次尝试都在工具超时时被杀,沙箱还会回收分离的后台进程。本 diff 不触碰任何 TypeScript、包清单或构建输入,因此这些门禁重跑的输入与检出时全绿的状态逐字节相同(运行器自己的前置 npm ci + npm run build,以及反馈中 CI 归因提到的该 head 上绿色的 Lint & Static 通道)。推送后的确定性门禁会重跑全部这些命令。

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/模型 kimi-k3

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

8 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • the assign POST's issues:assigned self-trigger in qwen-autofix.yml (.github/scripts/upsert-deferred-issue.sh:518) — already reported (comment 3941924819, R2-6 (A)); the author filed it as issue 11214
  • the added comment's 30s spawnSync child-timeout figure (scripts/tests/qwen-autofix-workflow.test.js:15343) — already recorded (round 6 deferral at test:15330, re-recorded as D7-2 in round 7)
  • the 14 new cases appended to the suite's single heaviest it(), spawn count 60 to 74 (scripts/tests/qwen-autofix-workflow.test.js:14354) — already reported (comments 3942634803 and 3943060577, R3-3), declined twice by the author
  • no fixture witnessing the codepoint-safety of the jq .[0:80] title cap (scripts/tests/qwen-autofix-workflow.test.js:14527) — already recorded (round 7 deferral)
  • the assign warning's captured reason never asserted (scripts/tests/qwen-autofix-workflow.test.js:14258) — already recorded (round 4 deferral at test:14351)
  • the author interpolated mid-clause in the published body sentence (.github/scripts/upsert-deferred-issue.sh:471) — already recorded (round 5 deferral)
  • the ready-for-agent comment arguing one state of a two-state gate (.github/scripts/upsert-deferred-issue.sh:486) — already recorded (round 6 deferral); re-derived by this round's round-4 auditor and rejected on that ground plus a measured f…
  • the review's empty build-and-test scope, both changed files sitting outside every npm workspace (.github/scripts/upsert-deferred-issue.sh:1) — already disclosed (round 5, 6 and 7 bodies)

Not reviewed: build-and-test — Test (ubuntu-latest, Node 22.x) was cancelled at its 120-minute ceiling before reaching npm run test:scripts, Test (macos-latest) and Test (windows-latest) were skipped, and this review's own scoped build-test ran no suite because both changed files sit outside every npm workspace.

Not reviewed: the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.

Not explored to full depth (tool budget reached): "agent 1c": could not execute scripts/tests/qwen-autofix-workflow.test.js to measure the heaviest case's post-diff runtime — the review worktree has no node_modules ( v….

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

  • .github/scripts/upsert-deferred-issue.sh:455 — [probe] Degradation gate enumerates derivations, not the response contract
  • .github/scripts/upsert-deferred-issue.sh:461 — [probe] Degradation warning promises persistence before the create runs
  • .github/scripts/upsert-deferred-issue.sh:471 — [probe] Degraded-path body prose has no witness for the guard's false side

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

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

本轮确认的 8 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):build-and-test — Test (ubuntu-latest, Node 22.x) was cancelled at its 120-minute ceiling before reaching npm run test:scripts, Test (macos-latest) and Test (windows-latest) were skipped, and this review's own scoped build-test ran no suite because both changed files sit outside every npm workspace.

未审查(原文为英文):the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.

未探索到全部深度(达到工具调用预算):"agent 1c"could not execute scripts/tests/qwen-autofix-workflow.test.js to measure the heaviest case's post-diff runtime — the review worktree has no node_modules ( v…

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

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round — no action

No commit this round. The feedback since the last evaluation contains no requested code change, and the two newly red checks are job-level timeout cancellations on the shared ECS pool with no code cause in this diff.

Feedback triage

  • Review [rv:5127066027] (@qwen-code-ci-bot, COMMENTED — "Partially reviewed — gaps disclosed") — requests nothing. Its 8 Suggestion-level findings are confirmed already-reported and keep their prior dispositions (one filed by the author as issue ci(qwen-autofix): prefilter issues.assigned so a create-time assignee does not start a doomed route job #11214; one declined twice; five recorded as deferrals in earlier rounds). Its 3 [probe] items were deferred by the reviewer itself under the round-8 convergence posture — "recorded, not requested in this round". Each was re-verified against the current code this round:

    • upsert-deferred-issue.sh:455 (gate enumerates derivations) — accurate as a description, but the tradeoff is already documented in the script's own comment (the 200 text/html proxy case; a .number-presence gate would false-positive on the suite's stub PR object). Not requested.
    • upsert-deferred-issue.sh:461 (warning promises persistence before the create runs) — a real but minor wording nit the reviewer explicitly did not request; the same sentence is echoed by the suite's own fixture comment.
    • upsert-deferred-issue.sh:471 (no witness for the guard's false side) — partially refuted: the degraded paths do have witnesses (prFetchFailed, prUnusableBody, botAuthor pin the bare title / no cc / no assignee; badLogin pins && vs ||). Only the positive bare-from PR #5 prose is unpinned.

    Because the reviewer deferred these itself, and any fix would sit inside this PR's footprint (so the defer-to-follow-up queue — for verified out-of-footprint findings — is not the right vehicle), this round leaves them to the reviewer's own follow-up tracking instead of implementing or re-recording them.

  • Inline comments: none. Issue-level comments: none.

  • Failed checks (new since last evaluation):

    • Test (ubuntu-latest, Node 22.x)CANCELLED at 22:09:50Z, 120m51s after start. The job's timeout on ECS runners is exactly 120 minutes (ci.yml), and per the round-8 review the cancellation hit before npm run test:scripts — i.e. during npm run test:ci:workspaces (the packages/* vitest suites). This diff touches no package source or package test; the only suite covering it (scripts/tests) was never reached. The workflow itself documents ECS contention stretching this lane past an hour (same shard measured at 6.7 vs 36 min by host placement, ci: Test (ubuntu) fails non-deterministically on shared runners — a different test set each run #10490).
    • web-shell E2E Smoke (ubuntu-latest, Node 22.x)CANCELLED at 22:30:17Z, 20m23s after start. Its own job timeout is 20 minutes (ci.yml); it builds and smoke-tests web-shell, which this diff does not touch.

    Both are job-timeout cancellations on a contended shared pool, not test failures; nothing in the diff can influence either, and no in-repo change in scope for this PR can prevent them. The remedy is a CI re-run (needs GitHub credentials this mode does not have); any subsequent push re-triggers the full lane.

Verification

Commands actually run against the checked-out head (439cd8e8, tree clean, matches origin):

  • bash -n .github/scripts/upsert-deferred-issue.sh — passed (syntax OK).
  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-autofix-workflow -t 'upserts deferred findings into a per-PR issue' — 1 passed (the PR's behavioral coverage: the suite's heaviest case, 74 subprocess spawns) in 16.7s.
  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-autofix-workflow (full file) — 237 passed (237), duration 161.5s.

No files changed; nothing committed.

中文说明

Autofix 本轮——无需改动

本轮不产生提交。 自上次评估以来的反馈不包含任何要求修改代码的内容,两个新变红的检查均为共享 ECS 资源池上的作业级超时取消,与本 diff 没有代码层面的因果关系。

反馈分诊

  • 评审 rv:5127066027——没有提出任何修改要求。其中 8 条建议级发现均被确认已在 PR 上报告过,保持既有处置(一条已由作者另立 issue ci(qwen-autofix): prefilter issues.assigned so a create-time assignee does not start a doomed route job #11214;一条已两次拒绝;五条已在先前轮次记录为延后)。其中 3 条 [probe] 条目是评审者自己在第 8 轮收敛姿态下延后的——"已记录,本轮不要求修改"。本轮已逐条对照当前代码复核:

    • upsert-deferred-issue.sh:455(降级门枚举派生值)——描述属实,但该取舍已写在脚本自身的注释中(200 text/html 透明代理情形;按 .number 存在性判门会对测试套件的 stub PR 对象误报)。本轮未被要求修改。
    • upsert-deferred-issue.sh:461(警告在 create 运行前就承诺持久化)——属实但属轻微措辞问题,评审者明确未要求修改;同一句话也被测试套件自身的夹具注释沿用。
    • upsert-deferred-issue.sh:471(守卫假分支缺少见证)——部分被证伪:降级路径确实有见证(prFetchFailedprUnusableBodybotAuthor 固定了裸标题/无 cc/无 assignee;badLogin 固定了 &&|| 之别)。只有正向的裸 from PR #5 文案未被固定。

    由于这些条目是评审者自己延后的,且任何修复都落在本 PR 自身的足迹之(因此用于"已核实但在足迹之外"发现的延后跟进队列并不适用),本轮将其留给评审者自己的跟进记录,既不实现也不重复登记。

  • 行内评论: 无。Issue 级评论: 无。

  • 失败的检查(自上次评估以来新增):

    • Test (ubuntu-latest, Node 22.x)——启动后 120 分 51 秒,于 22:09:50Z 被取消。该作业在 ECS 运行器上的超时恰为 120 分钟(ci.yml),且据第 8 轮评审,取消发生在 npm run test:scripts 之前——即 npm run test:ci:workspaces(packages/* 的 vitest 套件)阶段。本 diff 未触碰任何包源码或包测试;唯一覆盖本 diff 的套件(scripts/tests)根本没有被执行到。工作流自身就记录了 ECS 竞争会把该通道拉长到一小时以上(同一分片按宿主机位置实测 6.7 分钟与 36 分钟,ci: Test (ubuntu) fails non-deterministically on shared runners — a different test set each run #10490)。
    • web-shell E2E Smoke (ubuntu-latest, Node 22.x)——启动后 20 分 23 秒,于 22:30:17Z 被取消。该作业自身超时为 20 分钟(ci.yml);它构建并冒烟测试 web-shell,本 diff 未触碰该目录。

    两者均为竞争激烈的共享资源池上的作业超时取消,而非测试失败;diff 中没有任何内容能影响它们,本 PR 范围内的任何仓库内改动也无法预防它们。补救方式是重跑 CI(本模式没有 GitHub 凭据);任何后续推送都会重新触发整条通道。

验证

对已签出的头部提交(439cd8e8,工作树干净,与 origin 一致)实际运行的命令:

  • bash -n .github/scripts/upsert-deferred-issue.sh——通过(语法正确)。
  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-autofix-workflow -t 'upserts deferred findings into a per-PR issue'——1 通过(本 PR 的行为覆盖:套件中最重的用例,74 次子进程派生),耗时 16.7 秒。
  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-autofix-workflow(整个文件)——237 通过(237),耗时 161.5 秒。

未改动任何文件;未产生提交。

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


🧠 Handled by Qwen Code · model/模型 kimi-k3

@yiliang114

Copy link
Copy Markdown
Collaborator Author

CI attribution for the two failing required checks: runner-timeout cancellations, not this PR

Test (ubuntu-latest, Node 22.x) and web-shell E2E Smoke (ubuntu-latest, Node 22.x) render as fail in gh pr checks, but both job conclusions are cancelled — GitHub's job-level timeout-minutes fired on the shared self-hosted ecs-qwen fleet. Neither lane reached any code this PR touches.

Test job (run 34057060442, job 101550818833, runner ecs-qwen-hk3-18):

  • job started 20:09:02, step 16 Run tests and generate reports killed at 22:09:13 — 120 minutes exactly, the cap at ci.yml:384 (timeout-minutes: 120 when the runner resolves to ecs-qwen). The step's own 110-minute cap (ci.yml:699) never fired, so this is the job budget.
  • The kill landed inside the workspaces phase. The job's orphan cleanup names the processes still alive: npm run test:ci:workspaces --retry=2npm run test:ci --retry=2node (vitest) / esbuild. Since test:ci = test:ci:workspaces && test:scripts, the test:scripts lane never started — qwen-autofix-workflow appears zero times in the 2.7 MB job log. This PR's only test file, scripts/tests/qwen-autofix-workflow.test.js, lives in that unstarted lane.
  • Install dependencies alone took 21 minutes (20:09:3220:30:37), and there is an ~86-minute gap inside workspace vitest (20:32:5521:58:40).
  • Step 19 Publish Test Report (for non-forks) shows failure only as a downstream consequence: the killed step produced no report XML.

web-shell E2E Smoke (job 101567321344): started 22:09:54, cancelled 22:30:17 — 20m23s against timeout-minutes: 20 (ci.yml:1289), killed in step #11 Install dependencies. --log-failed for this job returns completely empty: no test failed, the job was killed. It never reached a browser or a test.

The same check is cancelled at the same ~20m2x wall on unrelated PRs, which makes it repo-wide rather than diff-specific:

PR job window wall conclusion runner
#11080 (this PR) 101567321344 22:09:5422:30:17 20m23s cancelled ecs-qwen-hk3-18
#11101 101511286923 15:27:2415:47:48 20m24s cancelled ecs-qwen-hk3-6
#9402 101477471687 11:19:5411:40:23 20m29s cancelled ecs-qwen-hk4-29

Decisive for this PR specifically: the live diff is exactly two files, and neither can reach the web-shell E2E surface or npm install time —

gh pr view 11080 --repo QwenLM/qwen-code --json files
  .github/scripts/upsert-deferred-issue.sh      +147/-8
  scripts/tests/qwen-autofix-workflow.test.js   +372/-5

Zero files in the diff match package.json, package-lock, web-shell or web-templates.

The two lanes alternate green/timeout across commits of this same PR — the signature of fleet contention, not of a code cause:

run head Test (ubuntu, 22.x) web-shell E2E Smoke
34021870820 3e05ec6401 cancelled @120m cap success (11m03s)
34031997859 239efbfc9b cancelled @120m cap success (9m26s)
34039883783 (attempt 2) 1b9b671d3f success (37m11s) cancelled @20M cap
34057060442 439cd8e872 (current head) cancelled @120m cap cancelled @20M cap

main is red in the same window, on the same fleet:

  • run 34076271669 (421393d5): Test = failure where every suite reported passed (Test Files 1015 passed (1015), Tests 28857 passed | 90 skipped, and each workspace shard passed) and the log then shows Terminated followed by ##[error]Process completed with exit code 1. The job's own DFSAMPLE lines record the host at load[155.50 144.77 138.71] immediately before the kill. Its Lint & Static was cancelled at its 90-minute cap (ci.yml:861).
  • run 34070970087: Lint & Static failure. Runs 34070804900 / 34070751704: cancelled.

This PR's own test file, run locally at head 439cd8e872:

npx vitest run scripts/tests/qwen-autofix-workflow.test.js --config ./scripts/tests/vitest.config.ts
 Test Files  1 passed (1)
      Tests  233 passed | 4 skipped (237)
   Duration  59.83s

Green, and ~60 seconds — it cannot account for a 98-minute test phase, and it never ran in the cancelled job anyway.

Verdict: category (ii), not caused by this PR — self-hosted runner contention and the job-level timeouts it provokes. This is the non-determinism already tracked in #10490, which the comment above that step (ci.yml:686-699) cites as the reason the caps and VITEST_RETRY exist ("a shard of identical work measures 6.7min or 36min depending only on which host it lands on").

Nothing is being fixed inside this PR for it, and the failing tests are not in its 2-file diff (.github/scripts/upsert-deferred-issue.sh, scripts/tests/qwen-autofix-workflow.test.js). Cheapest next step is a re-run of ci.yml for this head, ideally onto a less loaded host — left to maintainers, since re-triggering a required lane is their call rather than a code change.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round — no action

No code changes were made this round. The working tree remains at head 439cd8e872, clean, with the PR diff unchanged (2 files: .github/scripts/upsert-deferred-issue.sh, scripts/tests/qwen-autofix-workflow.test.js). Both feedback items explicitly conclude that nothing in this PR should change.

Feedback dispositions

  1. Inline [rc:3946640010] (@yiliang114, .github/scripts/upsert-deferred-issue.sh:518) — acknowledged; remains escalated, thread left open.
    This is a re-verification at the current head, not a new change request. It confirms: (B) the ready-for-agent hint still points at the per-item issue (fixed by 239efbfc9b, an ancestor of head), and (A) the issues.assigned event still passes route.if and spends one runner slot plus one permission call before exiting — deliberately not fixed here. Both candidate exits are out of bounds for this PR: dropping the create-time assignment would reverse the feature this PR ships, and adding an assignee-aware prefilter to route.if would edit shared CI routing in a file this PR never touches (and a creator-only guard would break the bot-assigned takeover path). The decision is tracked in ci(qwen-autofix): prefilter issues.assigned so a create-time assignee does not start a doomed route job #11214 with all constraints recorded. As the thread itself states, resolving it would record a decision nobody has made, so it stays open. No code change.

  2. Issue-level [ic:5565219859] (@yiliang114) — acknowledged; not a code problem.
    CI attribution showing the two failing required checks — Test (ubuntu-latest, Node 22.x) and web-shell E2E Smoke (ubuntu-latest, Node 22.x) — are job-level timeout-minutes cancellations on the shared self-hosted ecs-qwen fleet, corroborated by the same ~20-minute cancellations on unrelated PRs and by main being red in the same window. Neither lane reached any code this PR touches; the PR's only test file never even started in the killed job and passes locally in ~60s. Verdict: runner contention (tracked in ci: Test (ubuntu) fails non-deterministically on shared runners — a different test set each run #10490), not caused by this PR. The suggested next step — re-running the lanes, ideally on a less loaded host — is a maintainer action, not a code change.

  3. Still-red checks — nothing actionable. The only non-passing checks at head are the two CANCELLED jobs above, already covered by item 2. No check failure traces to the diff.

Verification

  • git status — clean tree at 439cd8e872, in sync with origin/ci/deferred-findings-issue-context.
  • git diff origin/main...HEAD --stat — confirmed the diff is still exactly the 2 files named above.
  • checks.json (this round's snapshot) — the only conclusions other than SUCCESS/SKIPPED are the two CANCELLED jobs attributed to runner timeouts in item 2.
  • No build/test/lint commands were run because no files were modified; there is nothing new to verify.
中文说明

Autofix 本轮 — 无需改动

本轮未做任何代码修改。工作树仍停留在 head 439cd8e872,处于干净状态,PR diff 未发生变化(仍为 2 个文件:.github/scripts/upsert-deferred-issue.shscripts/tests/qwen-autofix-workflow.test.js)。两条反馈都明确得出结论:本 PR 无需任何改动。

反馈处置

  1. 行内评论 rc:3946640010— 已知悉;维持升级处理,讨论串保持开放。
    该评论是在当前 head 上的复核确认,而非新的修改请求。它确认:(B) ready-for-agent 提示仍指向逐条创建的 issue(已由 head 的祖先提交 239efbfc9b 修复);(A) issues.assigned 事件仍会通过 route.if,在退出前占用一个 runner 槽位并产生一次权限调用 —— 这是有意不在本 PR 中修复的。两个候选修复方案都超出本 PR 的范围:去掉创建时的指派会回退本 PR 交付的功能;在 route.if 中增加感知指派人的预过滤则会改动本 PR 从未触及的共享 CI 路由文件(而且仅按创建者过滤还会破坏机器人被指派的接管路径)。该决策已在 ci(qwen-autofix): prefilter issues.assigned so a create-time assignee does not start a doomed route job #11214 中跟踪,所有约束均已记录。正如讨论串所述,此时标记 resolved 等于记录一个无人做出的决定,因此保持开放。本轮不改代码。

  2. Issue 级评论 ic:5565219859— 已知悉;并非代码问题。
    该评论的 CI 归因表明,两个失败的必需检查 —— Test (ubuntu-latest, Node 22.x)web-shell E2E Smoke (ubuntu-latest, Node 22.x) —— 是共享自托管 ecs-qwen 机队上 job 级 timeout-minutes 触发的取消;无关 PR 上同样出现约 20 分钟的取消、同一窗口内 main 分支同样变红,均佐证了这一点。两条链路都未执行到本 PR 触及的任何代码;PR 唯一的测试文件在被终止的 job 中甚至从未启动,本地运行约 60 秒即通过。结论:runner 资源竞争(已在 ci: Test (ubuntu) fails non-deterministically on shared runners — a different test set each run #10490 跟踪),与本 PR 无关。建议的下一步 —— 重新运行这些链路(最好落在负载较低的机器上)—— 属于维护者操作,而非代码改动。

  3. 仍然失败的检查 —— 无可操作项。 当前 head 上唯一未通过的检查就是上述两个 CANCELLED job,已包含在第 2 条中。没有任何检查失败能追溯到本 diff。

验证情况

  • git status —— 工作树干净,位于 439cd8e872,与 origin/ci/deferred-findings-issue-context 同步。
  • git diff origin/main...HEAD --stat —— 确认 diff 仍恰好是上述 2 个文件。
  • checks.json(本轮快照)—— 除 SUCCESS/SKIPPED 外,仅有的结论是第 2 条中已归因于 runner 超时的两个 CANCELLED job。
  • 由于未修改任何文件,本轮未运行构建/测试/lint 命令;没有需要验证的新内容。

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


🧠 Handled by Qwen Code · model/模型 kimi-k3

@wenshao

wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — real-environment run of upsert-deferred-issue.sh @ 439cd8e8

I built two independent harnesses rather than reading the diff, because almost every load-bearing claim in this PR is a claim about GitHub's actual behaviour, and the recording-gh suite can only assert what the stub was told to do.

  1. Real gh 2.46.0 → a local HTTPS GitHub-API server → the real script. No stub: a locally-trusted CA, GH_HOST=localhost:8443, real HTTP, real jq, real bash. Every request the script issues is recorded; faults (500, 200 text/html, assign-422, create-500) are injected at the server.
  2. The same script against real github.com, driven end-to-end on my fork against a PR whose title is 118 codepoints of hostile markdown, with a real inline review comment.

Verdict: the mechanism does what the description says, including the parts I expected to fail. Three findings below, all nits — none block merge. One process item does: CI has not run this PR's tests at the current head.


1. End-to-end, real gh, no stubs

  • Create path issues exactly the four calls claimed, in the claimed order: lookup → GET pulls/NPOST issuesPOST issues/N/assignees.
  • Append path issues zero pulls/ calls. The context fetch really is creation-only, so the steady-state round — the one that runs most often — spends no extra rate limit.
  • Degradation is real and it warns. pulls 500 and pulls 200 text/html both fall back to the bare title / no assignee / no cc, with the findings still persisted, and each prints a warning naming its own cause. The second gate is the interesting one: I confirmed against the real binary that gh api exits 0 on a 200 text/html body and copies it raw, so PR_FETCH_OK alone would have degraded silently. The ( -z TITLE && -z AUTHOR ) half of that gate is load-bearing.
  • Create is issued once and never retried (create-500 → exactly 1 POST, 0 assignee calls, LOST warning + raw dump). Assign-422 → 1 create POST, findings persisted, warning only.
  • CJK title, LC_ALL=C, 104 codepoints in → exactly 80 codepoints out, valid UTF-8. The equivalent bash ${x:0:80} byte slice truncates mid-sequence on the same input. The jq slice is not a stylistic preference.

2. The migration over the ~100 existing tracking issues — the risk I most wanted to disprove

I created a tracking issue with the merge-base script (bare rc:/rv:/ic: lines, no deep links), then ran the PR script against it with the same findings:

comments appended : 0     -> no duplicate wave

Counterfactual, same replay, with the deep-link suffix widened to ic/rv:

comments appended : 1     -> re-publishes 2 already-tracked items as new
  - ic:21 `src/x.ts`: an ic finding from the old rounds — [comment](…/pull/5#discussion_r21)
  - rv:31 `src/y.ts`: an rv finding from the old rounds — [comment](…/pull/5#discussion_r31)

So the review_comment-only restriction is not conservatism, it is the thing standing between this PR and a one-time duplicate wave across every existing tracking issue. Lookup identity also holds end to end: an enriched-title issue with the marker edited out is adopted, a legacy bare-title issue is still adopted, and PR #5 does not prefix-adopt PR #50's issue.

3. Real github.com — the claims a stub cannot settle

The title surface is plain text. Running the script for real produced this issue — the raw [URGENT: sign in to keep CI green](https://evil.example/phish) <details> renders as literal characters. No link, no fold. Assignee set by the separate call, cc @… in the body, rc: bullet deep-linked, rv: bullet deliberately bare, and round 2 appended only the new finding:

The body surface is not. I put the same title — neutralized exactly as the script escapes agent-derived text (ZWSP mention, &-entity, <!-- opener) — through GitHub's own POST /markdown. Left is the real body this PR ships; right is what a body copy would have produced:

The markdown link survives every neutralization byte-identical and renders live. The unclosed <details> makes GitHub inject a generic Details summary and nest the entire findings list inside it. Both halves of the rationale in the script's comment are correct, and the retired scriptEscapeSites census entry is the right way to keep them retired.

Assignment semantics — why a separate call. Against the real API:

call result
POST …/issues -f 'assignees[]=<non-collaborator>' HTTP 422 "assignees octocat cannot be assigned to this issue"no issue created
POST …/issues/<n>/assignees -f 'assignees[]=<non-collaborator>' exit 0, no stderr, assignees unchanged — a silent no-op

That settles it: assignees on the create call would have lost the findings outright for exactly the external contributors this branch exists for, and the separate call never warns for a non-assignable author — so the body cc really is the mechanism that reaches them, not a belt-and-braces extra.

The deep link is GitHub's canonical form. https://github.com/{repo}/pull/{pr}#discussion_r{id} is byte-identical to the html_url GitHub returns for a review comment; checked live against #11074's own finding id (…/pull/10991#discussion_r3937553394, HTTP 200).

Worst-case title length is fine. Deferred review findings from PR #12345: + 80 CJK codepoints = 95 codepoints / 203 bytes; real GitHub stored and returned it byte-identically.

4. A/B, regression parity, malformed shapes, CI

  • The tests are a gate. Merge-base source + this PR's own tests → red at qwen-autofix-workflow.test.js:14317.
  • Zero regressions. npm run test:scripts (82 files) on the same box: merge-base 15 failed | 2161 passed, PR head 15 failed | 2161 passed — identical. All 15 are pre-existing local-environment failures (uid 0, node_modules drift), including the one inside qwen-autofix-workflow.test.js.
  • 9/9 malformed PR-object shapes persist the findings (non-string .title, .user: null, no .user, whitespace title, [], a 200 error object, dependabot[bot], the autofix bot itself). Only the two genuinely-unusable fetches warn — the && gate does not warn on a healthy round with an unassignable author.
  • shellcheck --severity=style: 0 findings, both sides.

Findings

F1 — stale count in a sibling comment (nit). .github/scripts/autofix-push-and-report.sh:40 records "26 findings for run-autofix-review-verification.sh, 9 for upsert-deferred-issue.sh, 32 here". This PR takes that script to 10 under --enable=all (one new SC2312 at the gh api …/assignees … || echo line). The lane can't fail on it, but this repo treats comment counts as pinned facts elsewhere, so it's worth the one-character update.

F2 — whitespace-only PR title yields a dangling separator (nit, unreachable today). {"title":" "} passes [[ -n "${PR_TITLE_RAW}" ]], producing Deferred review findings from PR #N: with a trailing : . Harmless — the lookup's startswith($t + ":") still adopts it — and GitHub rejects blank PR titles, so it is only reachable if the fetch ever points at something other than pulls/N. Mentioning it because the script's own comment leans on "a whitespace-only title still flattens to a non-empty " as a desirable property of the warning gate; it is also what produces this.

F3 — the login charset guard's strictness is owned by another file (observation). [[ "${PR_AUTHOR}" =~ ^[A-Za-z0-9-]{1,39}$ ]] is locale-dependent in glibc. Measured on this box:

LC_ALL=C            ä=no     A=no
LC_ALL=en_US.UTF-8  ä=MATCH  A=MATCH

Production is safe: autofix-push-and-report.sh:430 launches the script through /usr/bin/env -i with no LANG/LC_ALL, so it runs in the C locale. Nothing to change — but the guard is the only thing keeping a forged login out of a cc @… mention published under the bot identity, and its strictness silently depends on a property set in a different file. A half-line reference to that (# strict under the env -i C locale the caller pins) would make the coupling visible.

Follow-up, not for this PR. rv:/ic: could carry #pullrequestreview-<id> / #issuecomment-<id> links later; the counterfactual above quantifies the cost of doing it naively (every persisted item re-published once). Doing it properly needs a dedupe migration, which is correctly out of scope here.

One thing to do before merge

Test (ubuntu-latest, Node 22.x) is cancelled at 439cd8e8 — and that is the job that runs npm run test:scripts. Lint & Static and Integration Tests (no-AK) are green, but CI has never executed this PR's new tests at this head. My local run is the substitute, not a replacement for the required check. Please push an empty commit or re-run that job before merging.

Recommendation

Approve. The design decisions I'd normally push back on — an extra API call on a hot path, appending text to lines that a dedupe mechanism compares, publishing contributor-controlled text under a bot identity — each turn out to be the ones the author already reasoned through, and every one of them holds under a real-environment test. F1–F3 are optional polish.

Harness: real gh → local HTTPS GitHub-API stand-in (fault-injecting, request-recording) + the real script; plus real github.com runs on my fork. The fork probe issues and the fixture branch were deleted after capture, so the screenshots are the surviving evidence.

中文版

Maintainer 验证 —— 在真实环境中运行 upsert-deferred-issue.sh @ 439cd8e8

我没有只读 diff,而是搭了两套独立的真实环境,因为这个 PR 的关键论断几乎都是关于 GitHub 真实行为的论断,而 recording-gh 测试套件只能断言"我们告诉 stub 去做什么"。

  1. 真实 gh 2.46.0 → 本地 HTTPS GitHub API 服务 → 真实脚本。 没有 stub:本地受信 CA、GH_HOST=localhost:8443、真实 HTTP、真实 jq、真实 bash。脚本发出的每个请求都被记录,故障(500、200 text/html、assign-422、create-500)在服务端注入。
  2. 同一个脚本跑真实 github.com:在我的 fork 上端到端驱动,目标 PR 的标题是 118 码点的恶意 markdown,并带一条真实的行内 review 评论。

结论:机制确实做到了描述所说的事,包括我原本预期会翻车的部分。 下面 3 条发现全是 nit,都不阻塞合并。真正需要在合并前处理的是一个流程问题:CI 从未在当前 head 上跑过本 PR 的测试。

1. 端到端,真实 gh,无 stub(截图 1)

  • 创建路径恰好发出所述的四个调用,顺序也一致:lookup → GET pulls/NPOST issuesPOST issues/N/assignees
  • 追加路径发出 0 个 pulls/ 调用,上下文拉取确实仅限创建路径,最常跑的稳态轮次不会额外消耗速率限制。
  • 降级是真的,而且会告警。 pulls 500 与 pulls 200 text/html 都回退到裸标题 / 无 assignee / 无 cc,findings 仍然落盘,且各自打印了指明原因的告警。第二个门更有意思:我用真实二进制确认了 gh api200 text/html退出码为 0 并原样拷贝正文——所以只看 PR_FETCH_OK 会静默降级。门里 ( -z TITLE && -z AUTHOR ) 这一半是承重的。
  • 创建只发一次、绝不重试(create-500 → 恰好 1 次 POST、0 次 assignee 调用、LOST 告警 + 原始转储)。assign-422 → 1 次创建 POST、findings 落盘、仅告警。
  • CJK 标题、LC_ALL=C、输入 104 码点 → 输出恰好 80 码点且是合法 UTF-8。同样输入下等价的 bash ${x:0:80} 字节切片会从 UTF-8 序列中间截断。jq 切片不是风格偏好。

2. 存量约 100 个 tracking issue 的迁移 —— 我最想证伪的风险

我先用 merge-base 脚本建出 tracking issue(裸 rc:/rv:/ic: 行、无深链),再用 PR 脚本以相同 findings 跑一轮:

comments appended : 0     -> 没有重复浪潮

反事实:同样的重放,但把深链后缀放宽到 ic/rv

comments appended : 1     -> 把 2 条已跟踪条目当作新条目重新发布

所以"只给 review_comment 加链接"不是保守,而是挡在这个 PR 和"存量 issue 上一次性重复浪潮"之间的那道墙。lookup 身份端到端也成立:marker 被编辑掉的增强标题 issue 会被收养、存量裸标题 issue 仍被收养、PR #5 不会前缀误吞 PR #50 的 issue。

3. 真实 github.com —— stub 无法裁决的论断(截图 3、4、2)

标题面是纯文本。 真实跑出的 issue 里,[URGENT: sign in to keep CI green](https://evil.example/phish) <details> 原样显示为字面字符,没有链接、没有折叠。assignee 由独立调用设置,正文有 cc @…rc: 条目带深链、rv: 条目刻意不带;第 2 轮只追加了新 finding。

正文面不是。 我把同一个标题——按脚本对 agent 文本完全一致的净化方式处理(ZWSP 提及、& 实体、<!-- 开头)——送进 GitHub 自己的 POST /markdown。左边是本 PR 实际发布的正文,右边是"若把标题复制进正文"的结果:markdown 链接原样存活并渲染成活链接;未闭合的 <details> 让 GitHub 注入一个通用 Details 摘要,并把整个 findings 列表嵌进折叠里。脚本注释里的两半论证都成立,退掉 scriptEscapeSites 计数点是正确的钉法。

assignment 语义 —— 为什么必须独立调用。 对真实 API:

调用 结果
POST …/issues -f 'assignees[]=<非协作者>' HTTP 422 "assignees octocat cannot be assigned to this issue" —— issue 根本没被创建
POST …/issues/<n>/assignees -f 'assignees[]=<非协作者>' 退出 0、无 stderr、assignees 不变 —— 静默 no-op

这就定案了:在创建调用上带 assignees,恰恰会让这个分支所服务的外部贡献者场景彻底丢失 findings;而独立调用对不可 assign 的作者根本不会告警——所以正文的 cc 才是真正触达作者的机制,而不是可有可无的补充。

深链就是 GitHub 的规范形式。 https://github.com/{repo}/pull/{pr}#discussion_r{id} 与 GitHub 返回的 html_url 逐字节一致;用 #11074 自己的 finding id 做了线上校验(HTTP 200)。

最坏情况的标题长度没问题。 Deferred review findings from PR #12345: + 80 个 CJK 码点 = 95 码点 / 203 字节,真实 GitHub 逐字节原样存储并返回。

4. A/B、回归对齐、畸形形态、CI(截图 5)

  • 测试确实是门。 merge-base 源码 + 本 PR 自己的测试 → 在 qwen-autofix-workflow.test.js:14317 变红。
  • 零回归。 同一台机器上 npm run test:scripts(82 个文件):merge-base 15 failed | 2161 passed,PR head 15 failed | 2161 passed,完全一致。15 个失败全是既有的本地环境问题(uid 0、node_modules 漂移),包括 qwen-autofix-workflow.test.js 里的那一个。
  • 9/9 种畸形 PR 对象形态都保住了 findings(非字符串 .title.user: null、无 .user、纯空白标题、[]、200 的错误对象、dependabot[bot]、autofix bot 自己)。只有两种真正不可用的拉取会告警——&& 门不会在"健康轮次但作者不可 assign"时误报。
  • shellcheck --severity=style:两侧均 0 findings。

发现

F1 —— 兄弟文件里的计数已过期(nit)。 .github/scripts/autofix-push-and-report.sh:40 写着"…9 for upsert-deferred-issue.sh…"。本 PR 把该脚本在 --enable=all 下的数量推到 10(新增一个 SC2312,位于 gh api …/assignees … || echo 那行)。该 lane 不会因此失败,但这个仓库在别处把注释里的计数当作被钉住的事实,值得改这一个字符。

F2 —— 纯空白 PR 标题会产生悬空分隔符(nit,当前不可达)。 {"title":" "} 能通过 [[ -n "${PR_TITLE_RAW}" ]],生成末尾带 : 的标题。无害——lookup 的 startswith($t + ":") 仍会收养它——且 GitHub 不接受空白 PR 标题,只有当这次拉取指向 pulls/N 以外的东西时才可达。之所以提,是因为脚本注释把"纯空白标题仍会 flatten 成非空的 "当作告警门的理想性质来依赖;同一性质也导致了这个现象。

F3 —— 登录名字符集守卫的严格性由另一个文件决定(观察)。 [[ "${PR_AUTHOR}" =~ ^[A-Za-z0-9-]{1,39}$ ]] 在 glibc 下依赖 locale。本机实测:

LC_ALL=C            ä=no     A=no
LC_ALL=en_US.UTF-8  ä=MATCH  A=MATCH

生产是安全的:autofix-push-and-report.sh:430 通过 /usr/bin/env -i 且不传 LANG/LC_ALL 启动脚本,因此运行在 C locale。无需改动——但这个守卫是唯一挡住"伪造 login 进入以 bot 身份发布的 cc @… 提及"的东西,而它的严格性却静默地依赖另一个文件设定的属性。加半行引用(# strict under the env -i C locale the caller pins)能让这个耦合可见。

后续(不属于本 PR)。 rv:/ic: 将来可以带 #pullrequestreview-<id> / #issuecomment-<id> 链接;上面的反事实量化了"天真地做"的代价(每条已落盘条目被重新发布一次)。要做对需要一次去重迁移,本 PR 把它排除在范围外是正确的。

合并前需要做的一件事

Test (ubuntu-latest, Node 22.x)439cd8e8 上是 cancelled,而这正是跑 npm run test:scripts 的 job。Lint & StaticIntegration Tests (no-AK) 是绿的,但 CI 从未在当前 head 上执行过本 PR 的新测试。我的本地运行是替补,不是必需检查的替代品。合并前请推一个空提交或重跑该 job。

建议

Approve。 我通常会去挑战的那几个设计选择——热路径上多一次 API 调用、往被去重机制比对的行上追加文本、以 bot 身份发布贡献者可控文本——结果都是作者已经想清楚的,而且每一条在真实环境测试下都成立。F1–F3 属于可选打磨。

验证环境:真实 gh → 本地 HTTPS GitHub API 替身(可注入故障、记录请求)+ 真实脚本;外加在我 fork 上对真实 github.com 的运行。fork 上的探针 issue 与 fixture 分支已在截图后删除,截图即留存证据。

@wenshao

wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (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: 675 passed · 0 failed · 675 total

Flakiness gate: ✅ 1 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

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

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

抖动门:✅ 1 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR #11080 — deep verification (round 2)

Verdict: findings — 675 scripted assertions, 675 pass / 0 fail. Verified head 439cd8e87245e47ad0eb5be1c54bbf4388ef9d2a (git rev-parse HEAD^2), base 92a8a8d17957b800548d6aaca7feb2916fbe6593 (HEAD^1), merge commit 1dd90b08.

This is a follow-up round; previous-report.md was present and every measurement in it was re-run at the new head rather than diffed. Nothing blocks. Both carried-forward Suggestions are resolved or materially reduced: the create is never retried coverage gap is fixed (the surviving mutant is now killed by exactly the fixture the last round proposed), and the mutation matrix went 12/13 → 19/19. What remains is four Nits and one standing correction to the PR description — plus a retraction of this reviewer's own Correction 2 from the last round, which a decisive experiment this round shows was wrong.

fail = 0, so merge-ready was permitted by the contract; findings is reported instead because the items below are concrete and worth a reviewer's eyes, not because anything failed.

中文摘要

结论:findings —— 675 条脚本化断言,675 通过 / 0 失败。验证 head 439cd8e8,base 92a8a8d1

这是第二轮:上一轮报告存在,本轮重新实测了其中每一项,而不是对比旧报告。没有任何阻断项。上一轮两条 Suggestion 均已解决或显著减轻:

  • Finding 1(create 永不重试 无测试 pin)已修复。变异体 M8(失败后重试一次 create)本轮被击杀,红在 :14647expected [ …(3) ] to have a length of 2 but got 3 —— 正是上一轮建议、作者采纳的那条 writeCreates 断言;同时那条误导性注释也被改写为"本用例并不 pin 这一点"。
  • Finding 2(最重用例耗时)机制复现、严重度下降。子进程数 60 → 74(+14)与增幅 +40.9%~+42.8% 精确复现(上轮 +43%),但本轮绝对耗时只有上轮的约 1/3.2(base 11.6–12.3s、head 16.6–17.3s),对 90s 预算的余量是 5.20–5.42×,而非上轮的 1.46–1.67×。绝对值受机器负载主导,两轮的绝对数字都不应被当作稳态。

变异矩阵 12/13 → 19/19 全部击杀,未变异对照 GREEN,正值对照被击杀,工作树 sha256 校验还原。见 02-mutation-matrix-19-of-19-killed.png

A/B 结论:中心主张成立且 load-bearing。真实脚本在 workflow 的 env -i 契约下跑两臂,15 个 cell 中 10 个存在差异,两臂都落盘 findings;最尖锐的 A9 cell 中 base 会再铸一个重复 tracking issue,head 收养不重复。675/675 断言通过。见 01-ab-head-vs-base-cells.png

本轮撤回上一轮自己的 Correction 2:上轮称"perSource 也 pin 住了 ic 侧,icTracked 属于冗余防御"。本轮用 M1b(只把深链后缀放宽到 issue_comment、不动 rv)做了判决性实验:M1b 被 :14922icTracked 击杀,而 perSource:14905 的 rv 断言保持沉默。所以作者注释"which perSource only pins for rv"是正确的,上一轮那条更正是错误的

Findings(详见下文):4 条 Nit —— 标题 flatten 未覆盖 \v \f U+0085 U+2028 U+2029(本 PR 新引入的暴露面);仅标题失效时降级静默无告警rv:/ic:-only 批次仍带 "Each rc: item links back…" 一句(上轮 Finding 3,仍在);fetch 退出 0 但带 stderr 时该 stderr 被丢弃(上轮 Finding 4,仍在)。Correction:PR 描述里"this case measures ~4.8s"在本 lane 的 runner 上仍不成立(实测 head 16.6–17.3s)。

未覆盖:逐 commit 归因(快照 10 个 commit,shallow 本地仅可达 1 个);GitHub 真实渲染与真实 API(无 token、无写操作);assign 调用的真实 wire body;整份 237 个测试的文件(只跑了 diff 触及的两个 -t 定向门禁);Windows/macOS;线上约 100 个存量 tracking issue(改由 base 脚本自己生成语料来模拟迁移)。

Previous-finding status

Every row was re-measured at 439cd8e8, not read off the old report.

# previous finding sev status at the new head
1 create is never retried unpinned; mutant M8 survived; a test comment claimed otherwise Suggestion FIXED. M8 is now KILLED at :14647AssertionError: expected [ …(3) ] to have a length of 2 but got 3. That is the writeCreates fixture the last round sketched, applied verbatim. The misleading assignFailed comment was also rewritten to state plainly that its own create succeeds and that the failed-create half is pinned by writeFail. Witness 03-m8-carried-forward-survivor-now-killed.png.
2 heaviest case 37.6 s → 53.9 s of a 90 s budget (margin 1.46–1.67×) Suggestion STANDS as a mechanism; severity materially reduced. The load-independent half reproduces exactly: runUpsert invocations in the block are 60 → 74 (+14), the same count as last round. The relative growth reproduces: +40.9 % (max) / +42.8 % (min) vs last round's +43 %. The absolute half does not: base 11 617/11 721/12 276 ms, head 17 215/17 299/16 594 ms — ≈3.2× faster than last round on both arms, so the margin is 5.20–5.42×, not 1.46×. Machine load dominates the absolute number; neither round's figure is the steady state. Witness 05-duration-base-vs-head-3-runs-each.png.
3 "Each rc: item links back to its original review comment." emitted on rc-less batches Nit STANDS. rv-only, ic-only and rv+ic batches all produce no rc: bullet and no deep link, yet all three carry the sentence. Persistence and the exemption itself are correct in all three.
4 a fetch that exits 0 with stderr discards that stderr Nit STANDS. With the stub writing gh: a diagnostic that would have named the proxy to stderr and an HTML body, the warning carries only the generic the call exited 0 but returned no usable PR object. Still strictly better than base, which has no warning on this path at all.
C1 Correction: the body's "this case measures ~4.8 s and times out" Correction STANDS, unaddressed. The sentence is unchanged in the PR body. Measured here: head 16.6–17.3 s, base 11.6–12.3 s. The advice to pass --config is right and more load-bearing than stated — against vitest's 5 s default the case overruns by 3.3×, not marginally.
C2 Correction: "perSource does pin the ic side as well, and icTracked is redundant defence" Correction RETRACTED — the previous round was wrong. See Corrections below; adjudicated by a dedicated experiment (m1b.mjs, 8/8). The author's comment is correct and icTracked is the sole witness for the ic side.
C3 Correction: "restoring that retry shape must red the single-create assertion below" Correction FIXED — the comment now says the opposite, and correctly (see row 1).

Scope

git diff --numstat HEAD^1..HEAD: .github/scripts/upsert-deferred-issue.sh +147/−8 and scripts/tests/qwen-autofix-workflow.test.js +372/−5. No workflow YAML, no manifest, no lockfile — asserted programmatically (assertNoDependencyConfound), so the base arm is a pure code control with no dependency confound and no internal workspace package on the code path.

Central claim: on the create path only, fetch the PR once and make the tracking issue self-describing — title bare: <PR title>, body naming PR number and author plus cc @author, every rc: bullet deep-linked — assign by a separate best-effort call, never retry the create, and on any context failure degrade to the bare form with a warning while still persisting. The PR title is published on the issue TITLE only and must reach the markdown BODY in no spelling.

Secondary claims, both verified: (a) the rv:/ic: deep-link exemption prevents a one-time duplicate wave over existing tracking issues; (b) the marker-less title fallback adopts both the bare and the enriched form, colon-guarded so PR #5 never prefix-adopts PR #50's issue.

Delta since the last round (the three commits after ba2b9851): the title→body copy was removed entirely and the escape-site census dropped 2 → 1; the ready-for-agent pointer moved to the per-item issue; the never-retried create was pinned. New probes were scoped to exactly these, plus re-measurement of everything above.

Central claim — A/B table

Both arms run the real shipped script, never a stub of it, under the production contract transcribed from .github/workflows/qwen-autofix.yml:6627-6655: an /usr/bin/env -i clean child exporting only the production variable set, doing the PAT bot-identity check, then bash -c "${UPSERT_SRC}" with the child's own stdout and stderr sent to /dev/null and messages carried on fd 3. Oracle per cell is the recorded gh argv (NUL-delimited, so a multi-line body cannot corrupt the log) plus the fd-3 stream the workflow consumes. Base-arm reds are encoded as expectations, so they count as passes. 675/675 across all harnesses; the A/B alone is 127/127. Witness: 01-ab-head-vs-base-cells.png.

cell oracle HEAD BASE (control)
A1 happy create -f title= argv …PR #5: Some PR title …PR #5 (bare)
A1 from PR #5 by someone in body present absent
A1 cc @someone. in body present absent
A1 #discussion_r7 in body present absent
A1 GET …/pulls/5 count 1 0
A1 POST …/issues/77/assignees count 1 0
A1 ready-for-agent pointer flow to that issue (per-item) (or apply the ready-for-agent flow) (ambiguous)
A2 fetch exit 1 + stderr warning reason bare + ;;error;;PAT lacks scope no warning exists
A3 fetch exit 0, 200 text/html warning bare + the call exited 0 but returned no usable PR object n/a (no fetch)
A4 assign rejected create POST count exactly 1 1
A5 author is the bot assign / cc 0 / none 0 / none
A6 login a@b, not a login, 40 chars, a/b, empty assign / cc 0 / none, no assignees token anywhere on the wire 0 / none
A7 append path GET …/pulls/ count 0 0
A9 marker-stripped enriched title adoption adopts #46, no fork FORKS a second tracking issue
A10 colon guard cross-PR adoption #5 never adopts #50's issue; #50 still adopts its own
A11 round-trip written title ↔ adopted title the title create writes is the title the lookup accepts, incl. past the 80-codepoint cap
A12 duplicate wave items re-published over a base-authored corpus 0 widening mutant: exactly 2 (rv + ic), rc still id-anchored
all arms findings persisted yes yes

A9 is the sharpest cell and it is the base arm that fails: base's fallback only tests == $t, so given the enriched title this PR now writes, base forks a duplicate tracking issue — the outcome the file ranks worst.

Facts established without A/B, because they are deterministic properties of tools rather than of this diff:

  • no:assignee really is in the scan filter. qwen-autofix.yml:656 sets AUTOFIX_ISSUE_EXCLUDES: 'no:assignee -linked:pr …' and :995 interpolates it into the ready-for-agent gh issue list --search. So the rationale for moving the pointer off this issue (which now carries an assignee) is correct as stated.
  • -f is a static string parameter. gh 2.100.0's own gh api --help documents -F/--field as the form with "magic type conversion" and -f/--raw-field as adding "a string parameter in key=value format". So -f "assignees[]=…" is the correct array wire form and a PR title beginning with @ cannot be read as a filename — verified behaviourally too (D10: such a title reaches argv verbatim and the create succeeds). 3 occurrences across 2 in-repo files already ship -f "labels[]=…" against the sibling array endpoint.

Corrections

Corrections to text, not requests to change code.

  1. This reviewer retracts Correction 2 from the previous round. Last round reported that perSource pins the deep-link exemption's ic side too and that icTracked is therefore "redundant defence rather than the sole witness". The inference was invalid: M1 (widen to every source) was killed at perSource's assertion :14905, but that assertion can only have fired on perSource's rv item — whose corpus line - rv:21 \?`: dupstops matching once a suffix is appended. Its ic item isfresh`, absent from the corpus, so it publishes with or without a suffix and pins nothing.

    Decisive experiment (m1b.mjs, 8/8): mutant M1b widens the suffix to issue_comment only, leaving rv untouched. It is KILLED at :14922expect(icTracked.calls).not.toContain('issues/42/comments -f body=') — while perSource's :14905 stays silent. So icTracked is the sole witness for the ic side, and the author's comment at :14907 ("which perSource only pins for rv") is correct as written. Reclassification: icTracked is load-bearing, not redundant defence.

  2. The PR body's "this case measures ~4.8 s and times out" is still wrong on this lane's hardware (carried forward, unaddressed). Measured 3× per arm on this runner: head 16.6–17.3 s, base 11.6–12.3 s. The --config advice is correct and stronger than stated — against vitest's 5 s default the case overruns by 3.3×, so it fails on any shared runner rather than marginally.

  3. The script's ::warning:: lines do not become GitHub annotations — and this is pre-existing, not introduced here. The workflow's line loop (qwen-autofix.yml:6660-6672) neutralizes every fd-3 line lacking the __upsert_trusted__ prefix, so ::warning:: reaches the step log as ;;warning;;. Measured on both arms: base's own could not create the deferred-findings issue warning is neutralized identically to the PR's new could not fetch PR #5 context warning. The PR's new warning is on the same channel as every other one in the file, its reason survives into the step log intact, and nothing is lost relative to base. Recording it because the description says the degradation must be "SAID, like every other gh failure path here" — it is, and the mechanism is the log line, not an annotation.

  4. The escape-site census is unchanged by this PR — the 2 → 1 trajectory is intra-branch only. expect(scriptEscapeSites).toHaveLength(1) appears in git diff HEAD^1..HEAD as context, not as a +/ line, and both arms' scripts contain exactly 1 gsub("&lt;!\-\-"; "…") site (counted per arm, not inferred). The second site existed only mid-branch — added and then removed again between the two heads (which commits, exactly, is out of reach on this depth-2 checkout) — so a reviewer reading the aggregate diff sees a number that never moved, plus a new comment explaining why "the second site this census used to count … is gone on purpose". The comment is accurate about the branch history and the assertion does pin the count at 1 (M18 above is killed by it); what it is not is coverage this PR added. Correcting this because the previous round's report described it as a changed assertion 1 → 2, and that description is wrong at this head.

Findings

All four are Nits. None blocks; none loses findings; every one reproduces with the command given.

1. The title flatten covers \r\n\t but not five other line-break characters (Nit)

PR_TITLE_RAW is flattened with gsub("[\r\n\t]+"; " "). The security-relevant pair — CR and LF — is covered, so there is no CRLF injection into the form field. But five further characters survive verbatim into the issue title, measured one at a time (D2):

character reaches the issue TITLE flattened
\n \r \r\n \t no yes
\v (VT, U+000B) yes no
\f (FF, U+000C) yes no
U+0085 (NEL) yes no
U+2028 (LINE SEPARATOR) yes no
U+2029 (PARAGRAPH SEPARATOR) yes no

In all nine cases the create still succeeds and nothing reaches the body. This is new surface: base never placed any contributor-controlled text in the title, so base has zero exposure here. Bounding it: the consequence is a control character inside a plain-text issue title — cosmetic, and GitHub's own title handling decides the rendering, which I could not observe (see Not covered). It is worth naming only because the block comment says "Flatten and cap only", which reads as broader than the character class delivers.

cd /__w/qwen-code/qwen-code && node tmp/pr11080-verify-20260907-061243/delta.mjs   # section D2
Minimal suggested fix (NOT applied, NOT measured — advisory only)
-    | gsub("[\r\n\t]+"; " ")
+    | gsub("[\r\n\t\u000b\u000c\u0085\u2028\u2029]+"; " ")

I did not apply and re-run this, so I am not claiming it is measured. Two things a maintainer should check before taking it: jq 1.6's handling of \u0085 inside a bracket expression, and whether the change perturbs the entityTitle fixture at :14540, which pins the flatten's current output byte for byte.

2. A title-only degradation is silent — the warning gate covers total failure only (Nit)

The gate is PR_FETCH_OK != 1 || ( -z PR_TITLE_RAW && -z PR_AUTHOR ), and the script's own comment scopes it correctly to "a fully degraded round". But that leaves a partially degraded round unannounced, and the || true on both jq derivations swallows the reason. Measured across eight shapes (D11):

.title in the PR object jq outcome issue title cc / assign warning
"" / null / absent "" BARE yes / 1 none
12345 / true / {"a":1} / ["a"] jq error, swallowed by || true BARE yes / 1 none
" " (whitespace) " " ENRICHED yes / 1 none — correct, and it confirms the comment's claim

In all eight the findings persist and exactly one create is issued, so nothing is lost — only the enrichment silently does not happen. Reachability, bounded: of these shapes only an empty/null/absent title is plausible from the real endpoint, and there the silence is arguably right (a PR with no title has nothing to enrich and there is no failure to report). The four non-string types require a structurally unexpected response — the same transparent-proxy class the gate's second half exists to name — and there the swallowed jq error means the round looks healthy. This is the same root cause as carried-forward Finding 4: the reason machinery only covers total failure.

cd /__w/qwen-code/qwen-code && node tmp/pr11080-verify-20260907-061243/delta.mjs   # section D11

3. Carried-forward: "Each rc: item links back…" on batches with no rc: item (Nit)

Unchanged since the last round. rv-only, ic-only and rv+ic batches each render no rc: bullet and no deep link, yet the body still asserts a property of items it does not contain. Cosmetic; no dedupe or rendering consequence, and the exemption itself is correct in all three (verified: no #discussion_r reaches any of them).

cd /__w/qwen-code/qwen-code && node tmp/pr11080-verify-20260907-061243/delta.mjs   # section D6

4. Carried-forward: a fetch that exits 0 with stderr discards that stderr (Nit)

Unchanged since the last round. PR_CTX_REASON is overwritten unconditionally when PR_FETCH_OK == 1, so a diagnostic gh wrote while still exiting 0 is replaced by the generic reason. Strictly better than base, which emits nothing on this path.

cd /__w/qwen-code/qwen-code && node tmp/pr11080-verify-20260907-061243/delta.mjs   # section D7

Consequences I tested that do not hold

Bounding these matters more than escalating the Nits, so these are the scarier readings I tried and disproved at the new head:

  • No injection through the contributor-controlled PR title — the surface is now gone. The last round swept an escape chain; this round there is no chain to break because the title never reaches the body. I swept 44 hostile payload classes (markdown link, image beacon, unclosed and closed <details>, &lt;!\-\-/-->, raw HTML, <img onerror>, four mention spellings plus \@ and ZWSP, ::error::, ##[, code span, fenced block, heading, table, blockquote, backslash escapes, shell expansion, \u0000, RTL override, CJK, emoji, all six line-break forms, %/+, bare @, --flag-injection, @filename-injection, empty, whitespace-only, 400 chars) with a canary placed first in each title so the 80-codepoint cap cannot make "absent from body" trivially true. Every canary is asserted present in the title argv (a positive control that the fixture reached the code) and absent from the body. Zero survivors across 501 assertions. Six fixtures additionally assert payload-specific witnesses absent — evil.example, URGENT, <details>, beacon.png, </details>, <script>, alert(1), ::error::, ##[group, victimuser, &#64;, &amp;#64;, &commat;, admin — while the body keeps - rc:7 and from PR #5 by someone. Witness 04-hostile-title-sweep-canary-absent-from-body.png.
  • No cross-PR adoption collision. Seven hostile titles (Deferred review findings from PR #50, 0: Deferred review findings from PR #50, :, :::, Deferred review findings from PR #5, empty, 200 chars) driven into PR TypeError in Authentication Selection Interface #5's created title: PR refactor(cli): update OpenAI API key prompt with Bailian URL #50's marker-less lookup never adopts PR TypeError in Authentication Selection Interface #5's issue and always creates its own, while PR TypeError in Authentication Selection Interface #5 still adopts its own. The mechanism is structural, not lucky — CREATE_TITLE always begins with the looking-up PR's own bare title, and the only reader is startswith($t + ":"), so a hostile suffix cannot forge another PR's prefix.
  • No duplicate wave over the ~100 existing issues. The corpus was not hand-typed: the base script authored it (rc + rv + ic in one batch), then head replayed the identical findings against that real base-authored body and re-published 0 items. The widening mutant re-published exactly 2 — the rv and ic lines — while rc stayed suppressed by its id anchor.
  • No codepoint damage. CJK-100, emoji-100, mixed-astral, combining-mark, exact-80, exact-81 and a 79-char-then-@ title all slice to ≤80 codepoints with no U+FFFD, round-trip as valid UTF-8, and remain adoptable by the lookup. The sharpest cut — @ landing at codepoint 80 — leaves a bare trailing @ in the title with no part of victimuser surviving anywhere.
  • No GH_ERR cross-contamination. With fetch and create both failing, the two warnings name their own distinct reasons (FETCH-REASON-unique / CREATE-REASON-unique) with neither leaking into the other; the assign warning names the assign reason and is not polluted by an earlier call.
  • Degenerate payloads never lose findings. 18 shapes (array, empty object, empty string, HTML, five non-string title types, null/absent user, null/numeric/object login, embedded \u0000, a nested-JSON-string title, whitespace title, a 5 000-char title) all exit 0, all persist, all issue exactly one create, and the warning fires exactly when both derivations came back empty.

Mutation matrix — 19/19 killed

Witness: 02-mutation-matrix-19-of-19-killed.png. Oracle is the PR's own suite via vitest's JSON reporter (testResults[].assertionResults[]) — never a regex over ANSI output, which is how the previous round's first attempt fabricated a fake 13/13. The unmutated control is GREEN (passed=1 failed=0) and the positive control is KILLED, so the harness demonstrably can make this suite fail. Each mutant is a single-point edit with an exactly-one-occurrence precondition and a bash -n check (on a real file — bash -n /dev/stdin is unavailable in this container) before use; each swaps the working-tree script, runs, and restores in a finally, with the final sha256 asserted equal to the recorded pristine a1c904706f542b72… and git status --porcelain confirmed empty. 0 build errors.

mutant claim under test verdict first red (qwen-autofix-workflow.test.js)
PC rename the dedupe marker positive control KILLED :14311 marker present in the create call
M1 widen deep-link suffix to all sources rv/ic exemption KILLED :14905 perSource rv side
M1b widen to issue_comment only adjudicates Correction 2 KILLED :14922 icTracked — sole ic witness
M2 drop the colon guard in the lookup enriched adoption KILLED :15200 adoption of #44
M3 drop the login-charset guard malformed .user.login KILLED :14415 badLogin
M4 gate on call status only warning gate's second half KILLED :14383 prUnusableBody warning
M5 delete the context warning observability KILLED :14360 prFetchFailed warning
M6 raw ${GH_ERR} instead of gh_reason() :: neutralisation KILLED :14360 ;;error;; vs ::error:: (same assertion M5 deletes)
M7 make the append path fetch too creation-only context KILLED :14582 append issues no pulls/
M8 retry the create on failure never retried KILLED ← was SURVIVED :14647 expected [ …(3) ] to have a length of 2 but got 3
M9 drop -n "${NUM}" from the assign guard no assign after failed create KILLED :14652 writeFail sees no assignees
M10 re-add the retired title→body copy title kept out of markdown KILLED :14512 body must not contain evil.example
M11 widen {1,39} to {1,} 40-char login KILLED :14446 longLogin
M12 admit @ into the charset class a@b login KILLED :14436 atLogin
M13 flip the degradation gate's && to || title-present / author-rejected KILLED :14425 no spurious warning
M14 delete the [\r\n\t] flatten title flatten KILLED :14540 expected undefined to be '…PR #5: …'
M15 delete the .[0:80] cap title cap KILLED :14540 exact capped string
M16 restore the ambiguous ready-for-agent parenthetical pointer aimed per-item KILLED :14335
M17 one-sided restyle of the ": " separator round-trip write ↔ adopt KILLED :14318
M18 plant a second gsub("&lt;!\-\-"; …) site census stays at 1 KILLED :16412 toHaveLength(1) — a pre-existing assertion, see Correction 4

Every declared mutation claim in the new test comments held, including the one that failed last round. Vacuity of the central new test is proven twice over (vacuity.mjs, 8/8): the head suite against the base script goes red at :14317 with AssertionError: expected '…' to contain 'api repos/o/r/pulls/5' — an assertion mismatch naming expected-vs-actual, not a crash — and a single-line revert that drops only the pulls fetch (leaving every precondition intact) reds the same assertion. Per the skill's rule on blunt reverts, the fine result is the one to trust: the test is pinned by the change itself.

The census is live, not decorative: the shipped script matches gsub("&lt;!\-\-"; "…") exactly once, and planting a second site makes the same regex count 2. CREATE_TITLE has exactly three occurrences — two writes and one read, at -f title="${CREATE_TITLE}" — so the enriched title has no second consumer.

Not covered

  • Per-commit attribution. The snapshot lists 10 commits; the depth-2 merge-ref checkout makes 1 reachable (git rev-list HEAD^1..HEAD^2 → only 439cd8e8, and git rev-parse --is-shallow-repositorytrue). A bare git rev-list --count returns a plausible 1 at a shallow boundary rather than erroring, so this was checked against the snapshot's commits array, not assumed. The last round could see 5 of its commits' messages; this round the three delta commits were identified from the snapshot's headlines and the aggregate diff only. I verified the aggregate HEAD^1..HEAD diff; the nine earlier commits' individual claims were not separately exercised.
  • baseRefOid disagreement, named rather than resolved. The snapshot's baseRefOid is 9c1c41a9… while the merge-ref's HEAD^1 is 92a8a8d1…. HEAD^2 matches the snapshot's headRefOid exactly (439cd8e8…), which confirms a merge-ref checkout, so HEAD^1 is the base the A/B used, per the CI contract. The snapshot OID was captured at a different moment against a moving main; I could not fetch to reconcile it (no token).
  • The real GitHub renderer and API. No token, no writes. The design premise that issue titles are stored and rendered as plain text — no markdown pass, no mention filter is what justifies leaving the title unescaped while keeping it out of the body. I verified the script implements that split (title raw, body never carries it, asserted per call so neither surface can be credited with the other's rendering) but I could not observe GitHub actually render a title. Every escaping assertion is against recorded argv, not rendered HTML. This is also the limit on Finding 1's severity: what GitHub does with a U+2028 in a title is unmeasured here.
  • The real gh wire body for the assign call. GH_DEBUG=1 does not print request bodies in this build, so the body was verified from gh api --help (gh 2.100.0's own manifest) plus 3 occurrences across 2 in-repo -f "labels[]=…" precedents, not from a captured payload. No network call was made against the API this round.
  • The full 237-test file. Two targeted -t gates only: the upsert block (which contains every new behavioural case; green, 16.6–17.3 s) and posts a human-handoff marker when review addressing reaches a terminal handoff (which contains the scriptEscapeSites census; green, 44 ms per-test / 3.1 s wall). That assertion is unchanged context in the aggregate diff — see Correction 4. Each -t run reports passed=1 failed=0 skipped=236, i.e. 237 tests in the file, counted from the JSON reporter rather than estimated; the two gates between them exercised 2 distinct tests, leaving 235 unrun.
  • Repo-wide gates. No npm run test:scripts, no eslint, no typecheck, no npm run build: the diff touches no TypeScript source and no package, and the two files it touches are covered by the gates above.
  • shellcheck ran directly, not through the repo wrapper. node scripts/lint.js --shellcheck ends its pipeline in sed, so its exit status is sed's and a clean 0 there is weak evidence. I ran shellcheck 0.11.0 with the repo's exact flags (--check-sourced --enable=all --exclude=SC2002,SC2129,SC2310 --severity=style --format=gcc --color=never) on both arms: head 10 findings (SC2154×4 warning, SC2312×6 note), base 9 (SC2154×4, SC2312×5). The delta is +1 SC2312 note at line 520, the new assign warning's $(gh_reason) — the identical idiom the file already uses at base. No new finding class, no error-severity finding. The gate's liveness was proven by planting SC2034 and SC2164 and confirming both are reported. The binary's sha256 was verified against SHELLCHECK_SHA256['linux.x86_64'] in scripts/lint.js before extraction (8c3be12b…4e227198, exact match).
  • actionlint / yamllint not run. Neither binary is present and pip3 install --user is not permitted here. Immaterial: the diff changes no YAML. I read qwen-autofix.yml at :656, :995 and :6618-6672 but did not lint it.
  • Windows / macOS. The script is bash + jq + gh; only Linux was exercised. The PR's own table marks both as CI-only.
  • The ~100 live tracking issues. No network/token, so the migration was simulated by having the base script author the corpus rather than by reading real issues. This reproduces the shape of the migration (base-authored bytes replayed through head), not the real issues' contents.
  • No calibration against a real emitted artifact. This is a script PR, not a workflow-step PR: there is no posted comment or uploaded file whose bytes a replay could be calibrated against, and previous-report.md is a report about the script, not an artifact the script emitted. So nothing here claims calibration. What substitutes for it is that every cell drives the shipped script itself, under the invocation contract copied from the workflow, with the base arm as a live control — including the contract's easily-dropped detail that the child's stdout and stderr both go to /dev/null and only fd 3 survives.
  • jq version. This container has jq 1.6; I did not establish which version the production runners use. All jq programs exercised here are 1.6-compatible, but a 1.7-only difference would not have shown up. This bears on Finding 1's suggested fix, which I therefore did not apply.
  • Duration measurements are single-machine. 3 runs per arm, sequential, in a quiet window after the matrix finished. I cannot reproduce a fast-machine or heavily-contended regime by repetition here; the 3.2× gap against last round's absolute numbers is itself the evidence that load dominates.

Methodology

Everything ran in the CI verify container (node:22-bookworm, node v22.23.2, bash 5.2.15, jq 1.6, gh 2.100.0) against refs/pull/11080/merge at 1dd90b08. The unit under test is the shipped bash script, executed for real — never stubbed — under the env -i child contract transcribed from .github/workflows/qwen-autofix.yml:6627-6655, including the bot-identity precheck and the fd-3-only output channel. The only fake is gh: a recording stub (stub-gh.sh) that appends each invocation's exact argv NUL-delimited with \x1e record separators, and emulates the two behaviours the script depends on — --jq (filter applied to the JSON response) and --paginate (pages merged into one array, never double-wrapped). Assertions read that argv, so they judge the wire rather than the script's narration.

The base arm is git show HEAD^1:.github/scripts/upsert-deferred-issue.sh. Both arms were snapshotted into this directory before any harness ran (script-head.sh a1c904706f542b72…, script-base.sh 6d1064cfb44f5792…) and every harness reads the snapshot, not the live path. That is a deliberate race guard: the mutation matrix swaps the live script in place, so a harness reading it concurrently could pick up a mutant and report it as head behaviour — a race that fabricates a result rather than crashing. The duration harness's base arm ran in a scratch worktree at tmp/base-tree (removed afterwards; git worktree confirmed gone and git status --porcelain empty), with both arms' script shas asserted against the snapshots before timing and the worktree's module resolution checked (readlink -f node_modules resolves inside the worktree, and the diff touches no package, so nothing crosses the workspace boundary). The diff changes no manifest or lockfile — asserted programmatically — so reusing the installed root node_modules is a clean control.

Harnesses, all in this directory with their raw logs and rerunnable by a maintainer: ab-suite.mjs (127 assertions, logs-ab.txt), delta.mjs (501, logs-delta.txt), attribution.mjs (9, logs-attribution.txt), vacuity.mjs (8, logs-vacuity.txt), m1b.mjs (8, logs-m1b.txt), matrix.mjs (20 encoded expectations, logs-matrix.txt + matrix.json), shellcheck-gate.sh (2 liveness checks, logs-shellcheck.txt), durations.mjs (measurements, logs-durations.txt + durations.json), ab-table.mjs (the captured table, logs-ab-table.txt), tally.mjs (derives assertions.json from those artifacts so every count is auditable, logs-tally.txt). 675 assertions, 0 fail.

Four harness defects were found and fixed during the round, and are recorded because each shaped intermediate output; all four were proven to be harness faults before any conclusion was drawn from them. (1) field() looked for a joined -f title= argv element, but bash passes -f and title=… separately — 27 A/B cells reported false reds until the oracle was fixed. (2) The first driver did not replicate production's > /dev/null 2>&1 + exec >&3, producing a spurious Bad file descriptor on stderr; the fd-3 form is now transcribed exactly. (3) The authorDegraded oracle matched a bare ' by ', which the body always contains as "deferred by the autofix loop", so it reported every author as usable and three total-degradation cells looked like gate failures. (4) bash -n /dev/stdin is unavailable in this container, which turned all 19 mutants into BUILD-ERROR on a dry run — caught by running --dry before spending 20 minutes of vitest. The dry run is also what confirmed all 19 edit targets occur exactly once.

Flakiness gate log

rounds=5 files=1 skipped=0
file scripts/tests/qwen-autofix-workflow.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/qwen-autofix-workflow.test.js


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  scripts/tests/qwen-autofix-workflow.test.js: PPPPP

verdict: pass
summary: 1 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)
round 2 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)
round 3 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)
round 4 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)
round 5 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)

Evidence images

01-ab-head-vs-base-cells

02-mutation-matrix-19-of-19-killed

03-m8-carried-forward-survivor-now-killed

04-hostile-title-sweep-canary-absent-from-body

05-duration-base-vs-head-3-runs-each

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

Qwen Code · sandboxed verification

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

APPROVE (verified at head 439cd8e)

Historical items

Never any CHANGES_REQUESTED on this PR — six review rounds, all Suggestions; the final round at this head posted zero new findings and carried eight, each dispositioned (the assign self-trigger was filed as issue #11214, the ready-for-agent two-state argument was rejected with a measurement, the added-cases count was declined twice with reasoning, the rest are assertion-strength notes). The human maintainer approved this head before this pass.

My Critical-only scan (injection and persistence surfaces)

The enrichment treats contributor-controlled input with the discipline the threat requires, and I verified each claim against the code rather than the comments:

  • The PR title reaches only the issue title (plain-text surface) — never the markdown body — with the exact live-phishing/<details>-collapse/dedup-corruption vectors spelled out as the reason; body-internal strings are either auto-linked (PR #N) or charset-validated (^[A-Za-z0-9-]{1,39}$ on the login before the deliberate @mention), and path/reason escaping keeps its existing chain with the rc deep-link suffix appended after the 500-char cap and justified safe by the id-anchored dedup identity (rv/ic deliberately unsuffixed to avoid a duplicate wave).
  • Title-cap slicing moved into jq (codepoint-safe) instead of a bash byte slice; the enriched title's lookup fallback (startswith($t + ":")) is guarded by the colon and paired with the creation site, stated where someone would restyle one and break the other.
  • Persistence stays non-idempotent-safe: one create call, never retried (duplicate-orphan reasoning explicit), assignment split out so it can never take the create down, external-author assignment decline handled by the body cc, every degradation path warns loudly instead of silently reverting to bare mode — and the warning reads before the error-reset that would wipe the reason.

The +372 lines are harness pins of the above (stub PR objects, refusal arms); I noted the round's own disclosure that Test (ubuntu-latest) was cancelled by this week's pool overrun before reaching test:scripts at this head, so those pins are unobserved in CI — a shared-infrastructure condition, non-attributable and non-gating per policy; CI at head otherwise shows 14 green and zero failures.

@wenshao
wenshao added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit dd5982a Sep 7, 2026
109 of 111 checks passed
@github-actions github-actions Bot added the skip-changelog-auto Automatically exclude internal CI changes from release notes label Sep 7, 2026
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.23.1.

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+) skip-changelog-auto Automatically exclude internal CI changes from release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants