Skip to content

fix(triage): finalize the status comment on cancellation too - #8436

Merged
wenshao merged 25 commits into
QwenLM:mainfrom
wenshao:fix/triage-status-comment-cancelled
Aug 7, 2026
Merged

fix(triage): finalize the status comment on cancellation too#8436
wenshao merged 25 commits into
QwenLM:mainfrom
wenshao:fix/triage-status-comment-cancelled

Conversation

@wenshao

@wenshao wenshao commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

The Finalize triage status comment step in qwen-triage.yml runs under (success() || failure()), which excludes cancellation — and both the job-level timeout-minutes: 30 (reachable on large PRs) and a manual cancel surface as cancellation. When that happens the step is skipped and the early "🔄 Qwen Triage is running" lifecycle comment keeps claiming the run is in progress forever (until some later trigger happens to overwrite the same marker). The verify lane has the separate publish-verify job to catch its cancelled case; the triage lane has no other publisher.

Changes

  • Run the step under always() && steps.resolve.outputs.number != '' — the number guard already keeps it out of runs that never resolved a target, and the step body is best-effort throughout (set -uo pipefail + || fallbacks on every write), so widening the condition cannot fail the job.
  • Branch on job.status for wording: a cancelled run now says "🚫 Qwen Triage was cancelled … re-run to get a result" (bilingual) instead of being silently skipped or mislabeled "ended early".
  • Update the workflow-shape test: pin the always() condition, the JOB_STATUS env, and the cancelled wording; negative-pin success() || failure() out.

Verification

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-triage-workflow.test.js — 120/120 pass
  • node --test .github/scripts/qwen-triage-workflow.test.mjs — all pass, 0 fail
  • YAML parse confirms the step's if and env render as intended
中文说明

摘要

qwen-triage.ymlFinalize triage status comment 步骤条件是 (success() || failure()),不覆盖取消——而 job 级 timeout-minutes: 30(大 PR 可能触及)和手动取消都以取消语义收场。此时该步骤被跳过,先前发出的 "🔄 Qwen Triage is running" 生命周期评论会永远停留在"进行中"(除非之后恰好有新触发覆盖同一 marker)。verify 车道有独立的 publish-verify job 兜底取消场景;triage 车道没有其他发布方。

变更

  • 步骤条件改为 always() && steps.resolve.outputs.number != ''——number 门保证未解析出目标的 run 不会误发评论;步骤本体已是全程 best-effort(set -uo pipefail + 所有写操作带 || 兜底),放宽条件不会使 job 失败。
  • job.status 分支措辞:取消的 run 现在显示 "🚫 Qwen Triage 已取消 …… 重新运行以获得结果"(双语),而不是被跳过或误标为"提前结束"。
  • 更新工作流形状测试:钉住 always() 条件、JOB_STATUS env 与取消措辞,并负向钉死 success() || failure() 不得回归。

验证

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-triage-workflow.test.js — 120/120 通过
  • node --test .github/scripts/qwen-triage-workflow.test.mjs — 全部通过
  • YAML parse 确认步骤 if 与 env 渲染符合预期

The 'Finalize triage status comment' step ran under
success() || failure(), so a job-timeout (timeout-minutes: 30, reachable
on large PRs) or a manual cancel skipped it and the early '🔄 Qwen
Triage is running' lifecycle comment kept claiming the run was in
progress forever — the verify lane has publish-verify to catch its
cancelled case, but the triage lane has no other publisher.

Run the step under always() (still gated on a resolved number), branch
on job.status to say the run was cancelled rather than 'ended early',
and point the reader at re-running. The step body is already
best-effort throughout, so widening the condition cannot fail the job.
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot qwen-code-ci-bot 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.

@wenshao The problem is real — (success() || failure()) never fires on cancellation, so when the job times out (or is cancelled manually) the finalize step is skipped and the "🔄 Qwen Triage is running" comment stays stuck until a later trigger happens to overwrite it — but the PR body doesn't follow the PR template. None of the required headings are present: What this PR does, Why it's needed, Reviewer Test Plan (with How to verify / Evidence (Before & After) / Tested on), Risk & Scope, Linked Issues. The body uses freeform headings (Summary / Changes / Verification) instead.

All the needed content appears to be there, so this should be a reformat rather than new work: SummaryWhy it's needed; ChangesWhat this PR does; VerificationReviewer Test Plan (How to verify keeps the shape-test commands you listed, Evidence (Before & After) can be N/A for a workflow-only change, Tested on filled in for what actually ran). Your "widening the condition cannot fail the job" reasoning belongs in Risk & Scope (main risk or tradeoff), and Linked Issues can just say none if nothing tracked this. Once the body follows the template, re-run with @qwen-code /triage and I'll continue the review.

中文说明

@wenshao 问题是真实存在的——(success() || failure()) 在取消时不会触发,所以 job 超时(或手动取消)时 finalize 步骤会被跳过,"🔄 Qwen Triage is running" 评论会一直卡住,直到某次后续触发恰好覆盖它——但 PR 正文没有按 PR 模板 填写。必需标题均缺失:What this PR doesWhy it's neededReviewer Test Plan(含 How to verify / Evidence (Before & After) / Tested on)、Risk & ScopeLinked Issues。正文用的是自由标题(Summary / Changes / Verification)。

所需内容其实都已具备,重新组织即可:SummaryWhy it's neededChangesWhat this PR doesVerificationReviewer Test PlanHow to verify 保留你列出的形状测试命令;纯 workflow 改动的 Evidence (Before & After) 可写 N/A;Tested on 按实际填写)。"放宽条件不会使 job 失败"的论证放进 Risk & Scope(主要风险或取舍);Linked Issues 若没有关联 issue 可写无。正文按模板调整后,用 @qwen-code /triage 重新触发,我会继续审查。

Qwen Code · qwen3.8-max

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline. Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-triage.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

中文说明

已审查——无阻断问题。 建议见行内评论。 未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-triage.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

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

Comment on lines +488 to +489
expect(finalizeStep).toContain("JOB_STATUS: '${{ job.status }}'");
expect(finalizeStep).toContain('Qwen Triage was cancelled');

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] R1-2: The new assertions pin the two halves of the cancelled branch (the JOB_STATUS env entry and the Qwen Triage was cancelled wording) but not the elif [ "${JOB_STATUS:-}" = 'cancelled' ] condition that wires them together — Concrete cost: mutating that condition to 'failure' keeps this suite green (mutation executed: the mutant survives, 1 passed), while a cancelled run — the exact case this PR exists to fix — falls through to the generic "ended early" wording, and a genuinely failed run gets labelled "cancelled".

Suggested change
expect(finalizeStep).toContain("JOB_STATUS: '${{ job.status }}'");
expect(finalizeStep).toContain('Qwen Triage was cancelled');
expect(finalizeStep).toContain("JOB_STATUS: '${{ job.status }}'");
expect(finalizeStep).toContain('Qwen Triage was cancelled');
expect(finalizeStep).toContain("elif [ \"${JOB_STATUS:-}\" = 'cancelled' ]");
中文说明

新增的断言钉住了取消分支的两半(JOB_STATUS env 项与 Qwen Triage was cancelled 措辞),但没有钉住把二者接起来的 elif [ "${JOB_STATUS:-}" = 'cancelled' ] 条件。具体代价:把该条件突变为 'failure' 后本测试仍然全绿(已实际执行突变验证:突变体存活,1 passed),而取消的 run——本 PR 要修的正是这个场景——会落入通用的 "ended early" 措辞,真正失败的 run 反而会被标成 "cancelled"。按上方 suggestion 补一行断言钉住该接线即可。

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

@wenshao

wenshao commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

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

Copy link
Copy Markdown
Collaborator

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

中文说明

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

@qwen-code-dev-bot

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

Copy link
Copy Markdown
Collaborator

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

中文说明

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

@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 轮)。改动内容与我反驳保留之处如下:

Feedback points and dispositions

1. [CHANGES_REQUESTED] Triage stage 1a — PR body must follow the PR template

Disposition: Escalated — cannot be applied from this workflow path; a ready-to-paste body is provided below.

Verified against the finding: the current body uses freeform headings (Summary / Changes / Verification) and none of the required template headings are present — the finding is correct.

Why the bot cannot fix it here: reformatting the PR body is a GitHub write. This agent runs without credentials, and the address-review path of the autofix workflow only pushes commits, resolves review threads, and replies to threads — it has no step that updates the PR title or body. Rather than silently skipping the point, I prepared the template-conformant body per the reviewer's mapping (SummaryWhy it's needed; ChangesWhat this PR does; VerificationReviewer Test Plan; the "widening the condition cannot fail the job" reasoning → Risk & Scope; Linked Issues → none). It is also saved as pr-title.txt / pr-body.md in this run's artifact.

Ask for a maintainer: apply the title/body below to the PR, then re-run @qwen-code /triage so stage 1a can continue.

Prepared template-conformant PR title and body (ready to paste)

Title:

fix(triage): finalize the status comment on cancellation too

Body:

## What this PR does

The qwen-triage workflow posts a "🔄 Qwen Triage is running" status comment early in the run and finalizes that same comment when the run ends. Previously the finalize step only ran on `success() || failure()`. This PR broadens the condition to `always()` (keeping the existing `steps.resolve.outputs.number != ''` guard), passes `${{ job.status }}` into the step as `JOB_STATUS`, and adds a dedicated finalization wording for cancelled runs: when triage did not complete and the job status is `cancelled`, the comment is finalized as "🚫 Qwen Triage was cancelled" with the run link, instead of the generic "ended early" wording.

## Why it's needed

`success() || failure()` never evaluates true when a job is cancelled — both the job-level `timeout-minutes` expiring and a manual cancel surface as cancellation. On that path the finalize step was skipped and the early "still running" comment stayed stuck until a later trigger happened to overwrite it. The verify lane has publish-verify to finalize its own status comment; the tmux lane only has this step, so it now covers every terminal outcome.

## Reviewer Test Plan

### How to verify

Workflow-only change. Coverage is the YAML shape test that pins the finalize step wiring:

```bash
npx vitest run --config ./scripts/tests/vitest.config.ts qwen-triage-workflow
```

Confirm the suite pins the `always() && steps.resolve.outputs.number != ''` condition, the `JOB_STATUS: '${{ job.status }}'` env entry, the `elif [ "${JOB_STATUS:-}" = 'cancelled' ]` branch, and the "Qwen Triage was cancelled" wording. Mutation check: changing the branch condition to `'failure'` makes the suite fail.

### Evidence (Before & After)

N/A — workflow-only change, no user-visible / TUI behavior.

### Tested on

|     OS     | Status |
| :--------: | :----: |
|  🍏 macOS  |   ⚠️   |
| 🪟 Windows |   ⚠️   |
|  🐧 Linux  ||

### Environment (optional)

Shape tests only (`scripts/tests/qwen-triage-workflow.test.js`); no CLI runtime involved.

## Risk & Scope

- Main risk or tradeoff: widening the condition cannot fail the job — the step already runs on the failure path today, and `always()` only adds the cancelled path. The script is best-effort (`set -uo pipefail`, warning instead of hard error), and the `steps.resolve.outputs.number != ''` guard is unchanged, so runs that cannot resolve a PR number still skip the upsert entirely.
- Not validated / out of scope: a genuinely cancelled GitHub Actions run cannot be replayed locally; the shape test pins the workflow wiring instead. The verify lane's finalize is untouched.
- Breaking changes / migration notes: none.

## Linked Issues

None — this was not tracked by an issue.

<details>
<summary>中文说明</summary>

本 PR 做了什么:qwen-triage workflow 会在运行早期发布 "🔄 Qwen Triage is running" 状态评论,并在运行结束时 finalize 同一条评论。此前 finalize 步骤只在 `success() || failure()` 时运行。本 PR 将条件放宽为 `always()`(保留既有的 `steps.resolve.outputs.number != ''` 守卫),通过 `JOB_STATUS``${{ job.status }}` 传入步骤,并为取消的运行新增专门的 finalize 文案:当 triage 未完成且 job 状态为 `cancelled` 时,评论会被更新为 "🚫 Qwen Triage was cancelled" 并附运行链接,而不是通用的 "ended early" 文案。

为什么需要:job 被取消时 `success() || failure()` 永远不为真——job 级 `timeout-minutes` 超时与手动取消都表现为 cancellation。该路径下 finalize 步骤被跳过,早期的 "仍在运行" 评论会一直卡住,直到某次后续触发恰好覆盖它。verify lane 有 publish-verify 来 finalize 自己的状态评论;tmux lane 只有这个步骤,因此它现在覆盖所有终态。

评审者测试计划——如何验证:纯 workflow 改动,覆盖是钉住 finalize 步骤接线的 YAML 形状测试(命令同上)。确认套件钉住了 `always()` 条件、`JOB_STATUS` env 项、`elif [ "${JOB_STATUS:-}" = 'cancelled' ]` 分支与 "Qwen Triage was cancelled" 文案;把该分支条件突变为 `'failure'` 会使套件失败。证据(改动前后):N/A(纯 workflow 改动)。测试环境:Linux ✅,macOS / Windows ⚠️(纯 workflow 改动,与 OS 无关)。环境:仅形状测试,不涉及 CLI 运行时。

风险与范围:主要风险或取舍——放宽条件不会使 job 失败:该步骤今天在失败路径本来就会运行,`always()` 只是补上取消路径;脚本尽力而为(`set -uo pipefail`,告警而非硬错误),`steps.resolve.outputs.number != ''` 守卫不变,无法解析 PR 编号的运行仍完全跳过 upsert。未验证 / 超出范围——本地无法重放真实的 GitHub Actions 取消运行,改以形状测试钉住接线;verify lane 的 finalize 未改动。破坏性变更:无。

关联 Issue:无——此改动未有 issue 跟踪。

</details>

2. [COMMENTED] Automated review — no blockers

Disposition: no action needed; the review's suggestions are tracked as the inline finding below.

3. [Suggestion] R1-2 — pin the cancelled-branch wiring in the shape test (rc:3702444880)

Disposition: Implemented. Added the assertion pinning elif [ "${JOB_STATUS:-}" = 'cancelled' ] in the finalize shape test, so the condition that wires the JOB_STATUS env entry to the "cancelled" wording is covered. The reviewer's mutation check was reproduced: with the condition mutated to 'failure' the suite now fails (1 failed | 119 passed), where the mutant previously survived; restoring the condition returns it to 120 passed. Prettier reformats the suggested line into a multi-line, single-quoted form — same assertion content.

Conflict notes

--conflict false — no merge performed; the branch applies cleanly on top of origin/main.

Verification

  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-triage-workflow — 1 file, 120/120 tests passed
  • Mutation check (workflow condition temporarily set to 'failure') — 1 failed | 119 passed as expected; after restoring the condition — 120/120 passed
  • npx prettier --check scripts/tests/qwen-triage-workflow.test.js — clean
  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0)
  • npm run generate:settings-schema — not needed (no settings source changed)
  • Integration tests after npm run bundle — not needed (the changed behavior is workflow YAML exercised only by the shape-test harness, not through the bundled CLI)
中文说明

反馈点与处理结论

1. [CHANGES_REQUESTED] Triage stage 1a —— PR 正文必须遵循 PR 模板

结论:升级人工处理——本工作流路径无法执行该修改;下方已提供可直接粘贴的正文。

已按 finding 核实:当前正文使用自由标题(Summary / Changes / Verification),模板要求的标题全部缺失——finding 属实。

为何 bot 无法在此修复:重排 PR 正文属于 GitHub 写操作。本 agent 无凭据运行,且 autofix 工作流的 address-review 路径只推送提交、解决评审线程、回复线程——没有更新 PR 标题/正文的步骤。为了不静默跳过该点,我已按评审者给出的映射准备好符合模板的正文(SummaryWhy it's neededChangesWhat this PR doesVerificationReviewer Test Plan;"放宽条件不会使 job 失败"的论证 → Risk & ScopeLinked Issues → 无),并同步存为本次运行 artifact 中的 pr-title.txt / pr-body.md

请维护者:将下方标题/正文应用到 PR 上,然后重新执行 @qwen-code /triage 以继续 stage 1a。(准备好的标题与正文见上方英文部分的折叠块,内容与 artifact 文件一致,其中已含中文翻译。)

2. [COMMENTED] 自动评审 —— 无阻断问题

结论:无需处理;该评审的建议即下方行内 finding。

3. [Suggestion] R1-2 —— 在形状测试中钉住取消分支的接线(rc:3702444880)

结论:已实现。 在 finalize 形状测试中新增断言,钉住 elif [ "${JOB_STATUS:-}" = 'cancelled' ],使把 JOB_STATUS env 项与 "cancelled" 文案接起来的条件被覆盖。已复现评审者的突变验证:把该条件突变为 'failure' 后套件失败(1 failed | 119 passed),此前该突变体存活;恢复条件后回到 120 passed。Prettier 将该建议行整理为多行、单引号形式——断言内容不变。

冲突说明

--conflict false —— 未执行合并;分支可干净地应用在 origin/main 之上。

验证

  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-triage-workflow —— 1 个文件,120/120 测试通过
  • 突变验证(workflow 条件临时改为 'failure')—— 按预期 1 failed | 119 passed;恢复条件后 —— 120/120 通过
  • npx prettier --check scripts/tests/qwen-triage-workflow.test.js —— 通过
  • npm run build —— 通过(exit 0)
  • npm run typecheck —— 通过(exit 0)
  • npm run lint —— 通过(exit 0)
  • npm run generate:settings-schema —— 无需(未改动 settings 源)
  • npm run bundle 后的集成测试 —— 无需(改动行为是 workflow YAML,仅由形状测试覆盖,不经 bundled CLI 执行)

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

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


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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline. Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-triage.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

中文说明

已审查——无阻断问题。 建议见行内评论。 未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-triage.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

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

Comment thread .github/workflows/qwen-triage.yml Outdated
ZH="✅ **Qwen Triage 已完成** —— [查看运行]($RUN_URL)。结果见本线程中的各阶段评论。"
elif [ "${JOB_STATUS:-}" = 'cancelled' ]; then
EN="🚫 **Qwen Triage was cancelled** — [view run]($RUN_URL). The run hit its time limit or was cancelled before finishing; re-run to get a result."
ZH="🚫 **Qwen Triage 已取消** —— [查看运行]($RUN_URL)。运行超时或被取消,未跑完;重新运行以获得结果。"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The new cancelled branch's Chinese wording is unasserted — no shape-test assertion references 已取消; only the EN half is pinned (scripts/tests/qwen-triage-workflow.test.js:489) — Failure scenario: executed mutant: replacing this ZH line's content with the English text keeps the suite 120/120 green, and Chinese-only readers get an English-only cancellation notice. This regression class has precedent in this suite: the re-run-summary test (~line 420) carries a CJK-presence check because that body once shipped English only. Add, next to the EN wording assertion:

expect(finalizeStep).toContain('已取消');
中文说明

新增取消分支的中文文案没有被任何断言钉住——形状测试中没有断言引用 已取消,只有英文半句被钉住(scripts/tests/qwen-triage-workflow.test.js:489)。失败场景:已实际执行突变——把该 ZH 行内容替换为英文文本后套件仍 120/120 全绿,纯中文读者将看到英文取消通知。本套件已有同类回归先例:re-run-summary 测试(约 420 行)正是因为该正文曾"只发英文"而加了 CJK 存在性检查。在 EN 文案断言旁补一行 expect(finalizeStep).toContain('已取消'); 即可。

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

if [ "${TRIAGE_OUTCOME:-}" = 'success' ]; then
EN="✅ **Qwen Triage finished** — [view run]($RUN_URL). See the stage comments in this thread for the result."
ZH="✅ **Qwen Triage 已完成** —— [查看运行]($RUN_URL)。结果见本线程中的各阶段评论。"
elif [ "${JOB_STATUS:-}" = 'cancelled' ]; then

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The three-way message selection is pinned by the shape test only as unordered toContain substrings, so which branch posts which message is untested — Concrete cost: executed mutant: swapping the success and cancelled EN+ZH bodies (conditions untouched, every asserted substring preserved) survives the full 120-test suite, while the real script observably posts the wrong message — extracted this step and ran it with a stubbed gh: under the mutant, (TRIAGE_OUTCOME=success, JOB_STATUS=success) posts "🚫 was cancelled". A real input reaching the order edge also exists: a timeout/manual cancel landing after the triage step already succeeded. The suite already solves this class elsewhere by executing the step's run: script against a stubbed gh and asserting the captured body (test lines ~244-288, ~335-403, ~2636+) — apply the same pattern to this composer, asserting the posted body for each (TRIAGE_OUTCOME, JOB_STATUS) combination.

中文说明

三分支消息选择在形状测试中只以无序 toContain 子串钉住,哪个分支发哪条消息未被测试。具体代价:已实际执行突变——交换 success 与 cancelled 的 EN+ZH 正文(条件不动、所有被断言子串保留)后整套 120 个测试仍全绿,而真实脚本会明显发错消息——提取该步骤并用打桩的 gh 运行:突变体下 (TRIAGE_OUTCOME=success, JOB_STATUS=success) 发出 "🚫 was cancelled"。存在可达分支顺序边界的真实输入:triage 步骤已成功后才发生的超时/手动取消。本套件在其他位置已用"执行步骤 run: 脚本 + 打桩 gh + 断言捕获正文"的模式解决同类问题(测试约 244-288、335-403、2636+ 行)——对这段消息组装应用同样模式,按每种 (TRIAGE_OUTCOME, JOB_STATUS) 组合断言所发正文。

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

Comment thread .github/workflows/qwen-triage.yml Outdated
Comment on lines +950 to +951
elif [ "${JOB_STATUS:-}" = 'cancelled' ]; then
EN="🚫 **Qwen Triage was cancelled** — [view run]($RUN_URL). The run hit its time limit or was cancelled before finishing; re-run to get a result."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The cancelled-path finalize PATCHes whatever lifecycle comment the marker lookup finds even when this run never posted its own claim — it can clobber a previous run's TERMINAL verdict wording written by qwen-triage-finalize.yml, which shares this marker — Concrete cost: executed probe: extracted this step and ran it with a stubbed gh against a fixture prior-verdict comment under (TRIAGE_OUTCOME=skipped, JOB_STATUS=cancelled) — it PATCHed the old verdict to "🚫 was cancelled — re-run to get a result". Trigger: a run manually cancelled within seconds of Resolve target number, before its own status claim lands; the verdict wording is then not restored (no further workflow_run completions fire once CI has fully settled). Impact is confined to this informational best-effort comment — the approval review event, stage comments, and CI table are untouched — hence Suggestion, not blocker. Fix (probe-verified to preserve the intended timeout-cancel-after-claim case): only PATCH when the found comment belongs to this run — skip when its body does not contain this run's $RUN_URL (the claim embeds the run link, so ownership is observable).

中文说明

取消路径的 finalize 会把 marker 查找命中的任意生命周期评论 PATCH 掉——即使本 run 从未发过自己的 "running" 声明——可能覆盖共用同一 marker 的 qwen-triage-finalize.yml 写入的上一个 run 的终态结论。具体代价:已实际执行探针——提取该步骤并用打桩的 gh 运行,在 (TRIAGE_OUTCOME=skipped, JOB_STATUS=cancelled) 下把一条旧结论评论 PATCH 成了 "🚫 was cancelled — re-run to get a result"。触发:run 在 Resolve target number 后数秒内(自己的状态声明发出前)被手动取消;此后结论文案不会被恢复(CI 完全落定后不再有新的 workflow_run 完成触发)。影响仅限于这条尽力而为的信息性评论——approve review 事件、各阶段评论与 CI 表格均不受影响——故为建议级而非阻断级。修复(已用探针验证不影响预期的"声明后超时取消"场景):仅当命中的评论属于本 run 时才 PATCH——其正文不含本 run 的 $RUN_URL 时跳过(声明评论内嵌运行链接,归属可判定)。

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

The cancelled finalize PATCHes whatever lifecycle comment the marker
lookup finds. When a run is cancelled before its own status claim
posts, the lookup lands on a previous run's comment — including the
terminal verdict wording written by qwen-triage-finalize.yml, which
shares the marker — and rewrites it to "was cancelled".

Only PATCH a comment this run owns: the claim embeds the run link, so
ownership is observable as the found body containing $RUN_URL. A found
but foreign comment is left untouched; a missing one is still POSTed.

Also pin what the shape test could not: the cancelled branch's Chinese
wording, and an executed-composer test that runs the step's script
against a stubbed gh to assert the posted body for each
(TRIAGE_OUTCOME, JOB_STATUS) combination plus the ownership cases —
swapping the success and cancelled bodies no longer survives the suite.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review response — PR #8436

All three automated-reviewer suggestions from the round-2 review were addressed in one commit (40140858e). No conflict handling was needed (--conflict false). Nothing was declined, deferred, or escalated this round.

Feedback and decisions

1. [rc:3704492687] The cancelled branch's Chinese wording is unasserted — ADDRESSED

Added expect(finalizeStep).toContain('已取消') next to the English wording assertion in the shape test. The new executed-composer test additionally asserts the Chinese half of every posted body, so replacing the ZH line's content with English text now fails the suite.

2. [rc:3704492695] Three-way message selection pinned only as unordered substrings — ADDRESSED

Added an executed test in the suite's existing stubbed-gh pattern: it extracts the finalize step's real run: script and runs it for each (TRIAGE_OUTCOME, JOB_STATUS) combination — (success, success), (success, cancelled), (cancelled, cancelled), (failure, cancelled), (failure, failure), (skipped, failure) — asserting the posted body contains the expected EN and ZH wording and does not contain the other branches' wording. The order edge the finding named (a timeout/manual cancel landing after the triage step already succeeded) is covered by (success, cancelled) → "finished". Verified the test kills the executed mutant: swapping the success and cancelled bodies makes it fail with expected '…🚫…' to contain 'Qwen Triage finished'.

3. [rc:3704492703] Cancelled-path finalize can clobber a previous run's terminal verdict — ADDRESSED

Verified against the code: the early status claim embeds this run's $RUN_URL, and qwen-triage-finalize.yml writes terminal verdict wording to the same lifecycle marker carrying the finalize run's URL — so a cancel landing before this run's claim posts made the marker lookup PATCH a foreign comment. The lookup now resolves to three states via jq: the found comment's id when its body contains this run's $RUN_URL (owned → PATCH), foreign when it does not (left untouched, with a warning), and empty when no comment exists (POST a new one — unchanged). The guard applies to every terminal branch, not only the cancelled one, because the lookup and the mutation are shared and the clobber class is identical. The intended timeout-cancel-after-claim case is preserved: the claim carries this run's link, so it still flips to terminal — pinned by the executed test's ownership cases.

Verification

Commands actually run and their results:

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-triage-workflow.test.js — 121/121 passed (120 existing + 1 new executed-composer test; jq 1.6 is present, so the jq-gated test ran rather than skipped)
  • Mutant check: applied the reviewer's success↔cancelled body swap to the workflow — the new test failed with {"TRIAGE_OUTCOME":"success","JOB_STATUS":"success"}: expected '…🚫…' to contain 'Qwen Triage finished'; the file was then restored and the full suite re-ran green
  • npm run lint — passed (exit 0)
  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npx prettier --check scripts/tests/qwen-triage-workflow.test.js — clean (file formatted with --write during the round)
  • Workflow YAML validity — parsed with js-yaml after the edit

Not run: integration tests after npm run bundle — the change is workflow YAML plus its shape/executed tests, which the bundled CLI and integration harness do not exercise. No settings source was touched, so npm run generate:settings-schema was not needed.

中文说明

Autofix 审查回应 —— PR #8436

第 2 轮评审中自动评审器提出的三条建议已全部在一个提交(40140858e)中处理。无需冲突处理(--conflict false)。本轮没有拒绝、推迟或升级任何反馈。

反馈与处理决定

1. [rc:3704492687] 取消分支的中文文案没有被断言 —— 已处理

在形状测试中英文文案断言旁补上了 expect(finalizeStep).toContain('已取消')。新增的"执行消息组装器"测试还会对每个发出的正文断言其中文半句,因此把该 ZH 行内容替换成英文文本现在会使套件失败。

2. [rc:3704492695] 三分支消息选择只以无序子串钉住 —— 已处理

按本套件已有的"打桩 gh + 执行真实脚本"模式新增了执行测试:提取 finalize 步骤真实的 run: 脚本,按每种 (TRIAGE_OUTCOME, JOB_STATUS) 组合运行 —— (success, success)、(success, cancelled)、(cancelled, cancelled)、(failure, cancelled)、(failure, failure)、(skipped, failure) —— 断言所发正文包含预期的英文与中文文案,且不包含其他分支的文案。反馈中点名的顺序边界(triage 步骤已成功后才发生的超时/手动取消)由 (success, cancelled) → "finished" 覆盖。已验证该测试能杀死执行突变体:交换 success 与 cancelled 的正文后测试以 expected '…🚫…' to contain 'Qwen Triage finished' 失败。

3. [rc:3704492703] 取消路径的 finalize 可能覆盖上一个 run 的终态结论 —— 已处理

已对照代码核实:早期状态声明内嵌本 run 的 $RUN_URL,而 qwen-triage-finalize.yml 会把带有 finalize run 链接的终态结论文案写入同一个 lifecycle marker —— 因此在本 run 的声明发出前就被取消时,marker 查找会 PATCH 到别人的评论。现在查找经 jq 解析为三态:命中的评论正文包含本 run 的 $RUN_URL 时返回其 id(本 run 所有 → PATCH);不包含时返回 foreign(保持不动并发出 warning);没有任何命中时为空(POST 新评论 —— 行为不变)。该守卫应用于所有终态分支,而不只是取消分支,因为查找与写入是共用的,覆盖(clobber)这一缺陷类别完全相同。预期的"声明发出后超时取消"场景得以保留:声明带有本 run 链接,因此仍会被翻转为终态 —— 由执行测试的所有权用例钉住。

验证

实际执行的命令及结果:

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-triage-workflow.test.js —— 121/121 通过(120 个已有测试 + 1 个新的执行组装器测试;jq 1.6 存在,故 jq 门控测试真实运行而非跳过)
  • 突变体检查:将评审器提出的 success↔cancelled 正文交换应用到工作流 —— 新测试以 {"TRIAGE_OUTCOME":"success","JOB_STATUS":"success"}: expected '…🚫…' to contain 'Qwen Triage finished' 失败;随后还原文件并重新完整跑套件,恢复全绿
  • npm run lint —— 通过(exit 0)
  • npm run build —— 通过(exit 0)
  • npm run typecheck —— 通过(exit 0)
  • npx prettier --check scripts/tests/qwen-triage-workflow.test.js —— 干净(本轮中已用 --write 格式化)
  • 工作流 YAML 合法性 —— 修改后用 js-yaml 解析通过

未执行:npm run bundle 之后的集成测试 —— 本次改动是工作流 YAML 及其形状/执行测试,打包后的 CLI 与集成测试框架均不涉及。未触及任何 settings 源文件,因此无需 npm run generate:settings-schema

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

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


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

@qwen-code-ci-bot

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.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review round: no action needed

Feedback triage. This round's feedback contains no review findings, no inline comments, and no maintainer requests — the only item is the automated reviewer's timeout notice: the review run exhausted its full 180-minute budget without producing a verdict, and the review-pr check failed as a result. There is nothing in the feedback that asks for a code change.

Failed check diagnosis. The only red check is review-pr, and it failed because the review itself timed out — not because of any defect reported about this change. Evidence:

  • The bot comment states the review timed out after 10800s (the entire budget); no findings were emitted.
  • This PR is small (2 files, +200/−16: a workflow step condition plus its tests), so the timeout is not plausibly caused by PR size or content.
  • All other checks are green (no entries under "Failed checks" besides review-pr, none persisting).

Local sanity check. To confirm the branch is healthy independent of the review timeout, the workflow test suite covering this change was run locally: npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-triage-workflow.test.js121/121 tests passed (including the new execution test that runs the finalize composer against a stubbed gh/jq; jq 1.6 present, so it was not skipped).

Why no code change. A timed-out review has no code-level remedy within this PR. The retry path named in the timeout notice — commenting @qwen-code /review --timeout=360 on the PR — is a GitHub write that the autofix bot is not permitted to perform (it has no credentials; the workflow owns all network writes). A maintainer or the workflow can trigger that retry if a review verdict is still wanted on this branch.

No commits were made this round; the branch head is unchanged at 40140858e.

中文说明

Autofix 审查轮次:无需任何操作

反馈分类。 本轮反馈不包含任何审查发现、行内评论或维护者请求 —— 唯一一条内容是自动审查器的超时通知:审查运行耗尽了完整的 180 分钟预算,未产出任何结论,review-pr 检查因此失败。反馈中没有任何要求修改代码的内容。

失败检查诊断。 唯一变红的检查是 review-pr,其失败原因是审查本身超时 —— 而不是本改动被报告了任何缺陷。证据如下:

  • 机器人评论说明审查在 10800 秒(即全部预算)后超时;未产出任何发现。
  • 本 PR 很小(2 个文件,+200/−16:一个 workflow 步骤条件及其测试),因此超时不太可能由 PR 的规模或内容引起。
  • 其他所有检查均为绿色("Failed checks" 中除 review-pr 外无其他条目,也没有持续失败的检查)。

本地健全性验证。 为确认分支健康状态与审查超时无关,在本地运行了覆盖本改动的 workflow 测试套件:npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-triage-workflow.test.js —— 121/121 个测试全部通过(包括新增的执行测试,该测试用桩 gh/jq 实际运行 finalize 组织脚本;环境中存在 jq 1.6,因此该测试未被跳过)。

为何不做代码修改。 审查超时在本 PR 范围内没有代码层面的补救办法。超时通知中给出的重试方式 —— 在 PR 上评论 @qwen-code /review --timeout=360 —— 属于 GitHub 写操作,autofix 机器人无权执行(它没有凭据;所有网络写操作由 workflow 负责)。如果仍希望在该分支上获得审查结论,可以由维护者或 workflow 触发该重试。

本轮未产生任何提交;分支头仍为 40140858e,未发生变化。

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


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

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ Downgraded from Approve to Comment: CI failing: review-pr. Reviewed. Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-triage.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

中文说明

⚠️ 已从批准降级为评论:CI failing: review-pr。 已审查。 未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-triage.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

— qwen3.7-max via Qwen Code /review (v0.21.4)

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [review-pr] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [review-pr] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review round: no action taken

The only new feedback on this PR is an automated COMMENTED review carrying zero findings (qwen-review-ledger round 1, "findings":[]). Its downgrade from Approve to Comment is attributed solely to "CI failing: review-pr", plus a note that the reviewer's own executable-script lint could not cover the workflow's embedded shell ("tool limitation, not a blocker"). No inline comments, no issue-level comments, and no failed or still-red checks were listed for this round.

Disposition of each feedback point

1. "CI failing: review-pr" — declined (no code action available).

  • review-pr is the automated LLM code-review job itself (qwen-code-pr-review.yml). That job fails only when the review run is aborted — timeout, model-API quota exhaustion, or a dropped/errored run — never because the verdict is COMMENT instead of APPROVE. A completed review posts its verdict and the check goes green.
  • This repository explicitly lists review-pr in NON_BLOCKING_CHECKS (qwen-autofix.yml), with the recorded rationale that the review's output "is a REVIEW, delivered by its own real-time pull_request_review trigger" and "the check conclusion carries nothing the loop acts on."
  • The very comment delivering this complaint is evidence that the latest review run completed and posted normally; a red review-pr observation alongside it reflects a transient/earlier run of the same job, which clears on re-run. Nothing in this PR's diff (the qwen-triage.yml finalize step and its workflow test) is executed by the review job, so no change here can influence that check.
  • The round's own feedback evaluation listed no failed checks and no still-red checks for this PR.

2. "Not linted … actionlint embedded-shell source mapping is not yet supported" — no action.
This is a report of the reviewer's lint-tooling coverage gap, explicitly marked "not a blocker", not a finding about this change.

Verification

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-triage-workflow.test.js121 passed (121) on the current HEAD (jq present, so the behavioral finalize test executed rather than being skipped).
  • Working tree clean; HEAD b257a03a9 matches origin/fix/triage-status-comment-cancelled. No commit was needed or made.
中文说明

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

本 PR 上唯一的新反馈是一条自动化 COMMENTED 审查,其发现数为零(qwen-review-ledger 第 1 轮,"findings":[])。它从 Approve 降级为 Comment 的唯一原因是 "CI failing: review-pr",外加一条说明:审查工具自身的 executable-script lint 无法覆盖工作流中的内嵌 shell 脚本("工具限制,非阻断")。本轮没有列出任何行内评论、Issue 级评论,也没有失败或持续飘红的检查项。

每条反馈的处理

1. "CI failing: review-pr" —— 拒绝处理(无代码层面的可操作项)。

  • review-pr 就是自动化 LLM 代码审查作业本身(qwen-code-pr-review.yml)。该作业只在审查运行本身被中断时失败——超时、模型 API 配额耗尽、连接中断或出错——绝不会因为审查结论是 COMMENT 而非 APPROVE 而失败。审查正常完成后会发布结论,检查项即为绿色。
  • 本仓库已在 NON_BLOCKING_CHECKS(qwen-autofix.yml)中显式将 review-pr 列为非阻断项,并记录了理由:审查的产出"是一条 REVIEW,通过其自身的实时 pull_request_review 触发器送达",且"该检查项的结论不携带任何本循环需要处理的信息"。
  • 投递本条抱怨的评论本身就证明最近一次审查运行已正常完成并发布;与其并存的 review-pr 飘红观测反映的是同一作业一次短暂失败/较早的运行,重新运行即可消除。本 PR 的改动(qwen-triage.yml 的 finalize 步骤及其工作流测试)不会被该审查作业执行,因此此处的任何改动都无法影响该检查项。
  • 本轮反馈评估中,该 PR 没有列出任何失败检查项,也没有持续飘红的检查项。

2. "Not linted … actionlint embedded-shell source mapping is not yet supported" —— 无需操作。
这是对审查工具自身 lint 覆盖缺口的说明,已显式标注"非阻断",并非针对本次改动的发现。

验证

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-triage-workflow.test.js —— 当前 HEAD 上 121 通过(共 121)(环境中存在 jq,因此行为级 finalize 测试实际执行而非被跳过)。
  • 工作区干净;HEAD b257a03a9origin/fix/triage-status-comment-cancelled 一致。无需提交,也未做任何提交。

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


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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

中文说明

已审查。 建议见行内评论。 未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-triage.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

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

Comment on lines +628 to +629
expect(foreign.body).toBe(null);
expect(foreign.call).toBe(null);

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] R3-1: The new ownership lookup is exercised only against single-comment fixtures, so the selector that picks which matching comment is classified (| last) is unpinned — executed mutant: lastfirst survives the full 121-test suite. Failure scenario: the common real-world shape is an issue that has seen more than one triage run, so the API returns the previous run's terminal comment (foreign) followed by this run's claim (own); the first mutant classifies the older foreign comment, takes the foreign) arm, and does nothing (no PATCH, no POST, exit 0), leaving this run's "🔄 running" claim permanently unflipped — exactly the stale-progress-comment bug this PR exists to fix. The fixture below fails against the mutant and passes against the original (both verified by execution).

Suggested change
expect(foreign.body).toBe(null);
expect(foreign.call).toBe(null);
expect(foreign.body).toBe(null);
expect(foreign.call).toBe(null);
// Both a prior run's terminal comment and this run's claim present:
// `last` must select the own claim, not classify the older foreign
// one and skip — a `first` mutant currently survives the suite.
writeFileSync(
commentsFile,
JSON.stringify([
{
id: 42,
user: { login: 'qwen-code-ci-bot' },
body: '<!-- qwen-triage lifecycle -->\n\n✅ earlier verdict [finalize run](https://github.com/QwenLM/qwen-code/actions/runs/55)',
},
{
id: 43,
user: { login: 'qwen-code-ci-bot' },
body: `<!-- qwen-triage lifecycle -->\n\n🔄 running — [watch live progress](${RUN_URL})`,
},
]),
);
const both = run({
TRIAGE_OUTCOME: 'failure',
JOB_STATUS: 'cancelled',
});
expect(both.call).toContain('--method PATCH');
expect(both.call).toContain('issues/comments/43');
中文说明

新增的归属查找只在"单条评论"的 fixture 上运行过,因此"对哪条命中评论做分类"的选择器(| last)没有被钉住——已实际执行突变:把 last 改成 first 后整套 121 个测试仍全绿。失败场景:真实的常见形态是同一个 issue 跑过多轮 triage,API 返回上一轮的终态评论(外部)在本轮的声明(自有)之前;first 突变体会把较旧的外部评论当作分类对象,走入 foreign) 分支什么都不做(不 PATCH、不 POST、exit 0),本轮的 "🔄 running" 声明将永远停留在"进行中"——这正是本 PR 要修的评论卡死问题。上方 suggestion 中的 fixture 对突变体失败、对原实现通过(均已实际执行验证)。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

中文说明

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

… lookup

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

Copy link
Copy Markdown
Collaborator

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

Autofix review round — PR #8436

Feedback points and dispositions

  • [rc:3710844914] Suggestion R3-1 — pin the finalize ownership selector (scripts/tests/qwen-triage-workflow.test.js:629): Addressed.
    The automated reviewer showed that the new ownership lookup in the finalize step is only exercised with single-comment fixtures, so the jq | last selector that decides which matching comment is classified was unpinned: a lastfirst mutant survived the full suite. In the common real-world shape (a prior run's terminal comment followed by this run's claim), the first mutant would classify the older foreign comment, take the foreign) arm, and leave this run's "🔄 running" claim permanently unflipped — the exact stale-comment bug this PR fixes.
    • Change: added the reviewer's two-comment fixture between the existing foreign and own cases — a prior run's terminal comment (id 42, no run URL) followed by this run's claim (id 43, embeds RUN_URL) — asserting the run PATCHes issues/comments/43. Verified by execution: with | last the suite passes 121/121; with a temporary lastfirst mutation of the workflow the new assertion fails (both.call is null because the script takes the foreign arm), so the mutant is now killed. The mutation was reverted afterwards; git diff origin/main...HEAD confirms only the intended test lines changed.
    • One wording adjustment to the suggested comment: "a first mutant currently survives the suite" → "would otherwise survive the suite", since after this change it no longer survives.
  • Review-level COMMENTED note (actionlint embedded-shell source mapping not linted): informational tool limitation, no action required.

Conflict notes

None (--conflict false; no merge performed).

Commit

  • c67187c7b test(triage): pin the last-comment selector in the finalize ownership lookup (1 file changed, 25 insertions)

Verification

  • npm run lint — passed
  • npm run build — passed
  • npm run typecheck — passed
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-triage-workflow.test.js — 121/121 passed
  • Mutation check: temporarily changed the finalize step's | last to | first, reran the targeted test — failed as expected (expect(both.call).toContain('--method PATCH'), mutant killed); restored the workflow and reran the full focused suite — 121/121 passed
  • npm run test:scripts (full scripts suite) — 893 passed, 9 skipped, 5 failed; all 5 failures are in scripts/tests/install-script.test.js with spawnSync zip ENOENT (the zip binary is not installed on this runner). Reproduced identically with this PR's test file reverted to its base version, so the failures are pre-existing and environment-specific, unrelated to this change.
中文说明

Autofix 审查轮次 — PR #8436

反馈点及处理

  • [rc:3710844914] Suggestion R3-1 — 钉住 finalize 归属查找的选择器scripts/tests/qwen-triage-workflow.test.js:629):已处理。
    自动审查者指出:finalize 步骤中新增的归属查找只在"单条评论"的 fixture 上运行过,因此决定"对哪条命中评论做分类"的 jq | last 选择器没有被钉住——把 last 改成 first 的突变体在整套测试中依然存活。在真实的常见形态下(上一轮的终态评论在前、本轮的声明在后),first 突变体会把较旧的外部评论当作分类对象,走入 foreign) 分支什么都不做,本轮的 "🔄 running" 声明将永远停留在"进行中"——这正是本 PR 要修的评论卡死问题。
    • **改动:**在现有的 foreignown 用例之间加入了审查者给出的双评论 fixture——上一轮的终态评论(id 42,不含本轮运行链接)在前,本轮的声明(id 43,内嵌 RUN_URL)在后——并断言本轮会 PATCH issues/comments/43。已实际执行验证:| last 时整套测试 121/121 通过;将 workflow 临时突变为 lastfirst 后,新断言失败(both.callnull,因为脚本走入了 foreign 分支),突变体被杀死。突变随后已还原;git diff origin/main...HEAD 确认只有预期的测试行变更。
    • 对建议中的注释做了一处措辞调整:"a first mutant currently survives the suite" → "would otherwise survive the suite",因为改动落地后该突变体已不再存活。
  • 审查级别的 COMMENTED 说明(actionlint 尚不支持 embedded-shell source mapping,未检查):属于工具限制的信息性说明,无需处理。

冲突说明

无(--conflict false;未执行合并)。

提交

  • c67187c7b test(triage): pin the last-comment selector in the finalize ownership lookup(1 个文件变更,新增 25 行)

验证

  • npm run lint — 通过
  • npm run build — 通过
  • npm run typecheck — 通过
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-triage-workflow.test.js — 121/121 通过
  • 突变检查:将 finalize 步骤的 | last 临时改为 | first 后重跑目标测试——按预期失败expect(both.call).toContain('--method PATCH'),突变体被杀死);还原 workflow 后重跑完整聚焦套件——121/121 通过
  • npm run test:scripts(scripts 完整套件)— 893 通过、9 跳过、5 失败;5 个失败全部位于 scripts/tests/install-script.test.js,报错为 spawnSync zip ENOENT(本 runner 未安装 zip 二进制)。将本 PR 的测试文件还原到基线版本后失败完全复现,因此属于既有的环境问题,与本改动无关。

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

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


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

@qwen-code-ci-bot

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.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [review-pr] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [review-pr] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@wenshao

wenshao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Correction to §5 of my round-5 report above — the two npm-ci- cache-key lines are not unrelated churn. They are required by the formatter, and the PR is fixing a pre-existing violation on main.

I had cited node scripts/lint.js --prettier exiting 0 as evidence that main was already clean. That script runs prettier --write . (scripts/lint.js:208) — it is a fixer, not a check, so its exit code proves nothing about the input. Re-checked properly:

$ cd <main @ 95655da>; npx prettier --check .github/workflows/qwen-triage.yml
[warn] .github/workflows/qwen-triage.yml
[warn] Code style issues found in the above file.        # main FAILS the check

$ npx prettier --write .github/workflows/qwen-triage.yml
1184:  key: 'npm-ci-${{ hashFiles(''package-lock.json'') }}'   ->   key: "npm-ci-${{ hashFiles('package-lock.json') }}"
2650:  key: 'npm-ci-${{ hashFiles(''package-lock.json'') }}'   ->   key: "npm-ci-${{ hashFiles('package-lock.json') }}"

$ diff <(grep -o 'key:.*npm-ci.*' main-after-write) <(grep -o 'key:.*npm-ci.*' pr-head)
                                                    # identical — prettier's own output

So prettier --write on main reproduces the PR's two lines byte-for-byte, and PR head passes prettier --check on both changed files from pristine. Nothing else in §5 changes: the parsed-YAML leaf diff is unaffected (those lines are semantically identical either way — '' is the YAML escape for '), and the blast-radius conclusion stands. Drop the "unrelated churn" characterisation; it should read that the PR carries two formatter fixups main currently needs.

中文版

更正上一条报告第 5 节 —— 那两行 npm-ci- 缓存 key 不是无关改动,而是格式化工具要求的,本 PR 是在修复 main 上既有的格式违规。

我此前用 node scripts/lint.js --prettier 退出码为 0 作为"main 本来就干净"的依据。但该脚本执行的是 prettier --write .scripts/lint.js:208)——它是修复器而非检查器,退出码无法说明输入是否合规。正确复核结果:mainnpx prettier --check 不通过prettier --write 后产生的两行与 PR head 逐字节一致;PR head 在未经改写的原始状态下 prettier --check 通过。

第 5 节其余内容不变:这两行在解析后语义完全相同('' 是 YAML 中 ' 的转义),叶子值比对结果与影响面结论均不受影响。请把"无关改动"的说法改为:本 PR 顺带修复了 main 目前需要的两处格式化问题。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Round 9 feedback: both inline findings are resolved with minimal changes (one added test assertion, corrected step comments). The standing Critical R8-9 (PR body not template-compliant) cannot be applied by this agent — an updated paste-ready body is included below for the PR author or a maintainer. No base-conflict resolution was needed this round (--conflict false).

1. [Suggestion] R9-2 — the claim's --jq '.id' extraction was never exercised by any test — RESOLVED.

First reproduced the reported mutant: removing --jq '.id' from the claim's POST arm kept all 129 tests green (the stub gh answers a POST with the already-extracted id, so nothing executes the flag). Applied the reviewer's cheaper option (2): a static pin beside the existing claim-step assertions — expect(statusStep).toContain("--jq '.id'") — with a comment recording why the executed harness cannot catch this mutant. Re-running the same mutant after the pin makes the suite fail on that assertion, so the named failure scenario (flag dropped → production COMMENT_ID captures the full created-comment JSON → malformed $GITHUB_OUTPUT write → finalize PATCHes a garbage id → lifecycle comment stuck "running") is now caught. Option (1) — teaching the stub to emulate --jq — was deliberately skipped: it would grow the shared harness without catching anything the pin does not.

2. [Suggestion] R9-3 — the empty-id no-op drops the pre-PR finalize recovery path — RESOLVED (documentation option).

Applied the reviewer's second option: the finalize step comment now documents the accepted loss. An empty id has two causes — a cancel landing before the claim posted, or a transient failure of the claim's write (both the fresh-POST and the PATCH arm export an empty id on failure) — and finalize deliberately writes nothing in either case. The comment now states that a failed claim therefore leaves no lifecycle breadcrumb even when the comments API recovers during the minutes-long agent run, and why that is accepted: the verdict still lands in the stage comments, and a list-then-POST recovery would reintroduce exactly the comment selection this PR removes (the suite already pins finalize to no listing at all: not.toContain('--paginate'), not.toContain('gh api user')). The matching test comment was aligned to the same two-cause wording. The guarded POST fallback (option 1) was declined as it re-adds the listing logic this change exists to remove and would contradict those existing assertions.

3. [Critical] R8-9 — the PR body must follow the PR template — NOT APPLICABLE BY THIS AGENT; maintainer/author action required.

Unchanged since triage stage 1a (review 4841776175): the description still uses freeform Summary/Changes/Verification headings and has no Test Plan section. This agent has no channel to edit the PR description — the address-review flow's GitHub writes are limited to pushing commits, posting comments, and resolving/replying to review threads. The paste-ready body drafted earlier (issue comment 5196984782) described the previous ownership-match iteration and is stale; the updated template-compliant body below reflects the current id-handoff design. After applying it, please re-run @qwen-code /triage so stage 1a can continue.

Updated paste-ready PR body (template-compliant, current id-handoff design)
## What this PR does

The triage workflow posts a lifecycle status comment ("🔄 Qwen Triage is running") when a triage run starts, and a final 'Finalize triage status comment' step flips that same marker comment into a terminal state. Previously each step re-ran its own marker-comment lookup, so the two could disagree about which comment this run owned — a selection that could race — and the finalize gate `(success() || failure())` was never true on cancellation (a job-level timeout or a manual cancel), so on that path the step was skipped and the status comment kept claiming the run was still in progress.

This change couples the two steps through an explicit id handoff instead of a second lookup: the claim step upserts the marker comment and exports the exact comment id it created (extracted with `--jq '.id'`) or reused, and the finalize step PATCHes exactly that id — nothing is re-derived at finalize time. The finalize gate is widened to `always()` so cancellation is covered too, and `${{ job.status }}` is passed in as `JOB_STATUS` so a dedicated cancelled terminal wording is selected alongside the existing finished / ended-early wordings, with a step-level success no longer allowed to claim "finished" when the job itself failed.

## Why it's needed

A stuck "running" claim misleads everyone watching the issue: the run ended, but the comment says it is still in progress, and nothing in this lane ever corrects it — the verify lane has publish-verify for exactly this case, while this lane only has the finalize step. Widening the gate to `always()` covers the one remaining path (cancellation) on which the comment could be left stale, and keying finalize on the claim's exported id removes the two-step selection disagreement entirely.

## Reviewer Test Plan

### How to verify

Run the workflow shape test that pins this behavior: `npx vitest run scripts/tests/qwen-triage-workflow.test.js --config ./scripts/tests/vitest.config.ts` (129 tests). It asserts that the claim step exports `comment_id` to `$GITHUB_OUTPUT` and extracts the created id with `--jq '.id'`; that the finalize step reads `STATUS_COMMENT_ID` from the claim step's output and PATCHes exactly that id while doing no comment listing at all (no `--paginate`, no `gh api user`); that the finalize `if:` condition is `always() && steps.resolve.outputs.number != ''`; the three-way wording selection (finished / cancelled / ended-early) driven by `steps.triage.outcome` and `job.status`; and the empty-id no-op. The claim and finalize scripts are additionally executed against a stubbed `gh` with failure-injection arms, asserting the exact body PATCHed for every terminal state, newest-marker reuse, legacy-marker reuse, and that a failed write exports an empty id without failing the step.

### Evidence (Before & After)

N/A — workflow-only change, no TUI surface.

### Tested on

|     OS     | Status |
| :--------: | :----: |
|  🍏 macOS  |   ⚠️   |
| 🪟 Windows |   ⚠️   |
|  🐧 Linux  ||

### Environment (optional)

N/A — unit/shape tests only (vitest).

## Risk & Scope

- Main risk or tradeoff: finalize PATCHes exactly the id the claim step exported. When the claim's write fails transiently — or a cancel lands before the claim posts — the exported id is empty and finalize deliberately writes nothing, so the thread loses this run's lifecycle breadcrumb even though the comments API typically recovers during the run; accepted because the verdict still lands in the stage comments and a fallback post would reintroduce the selection this change removes (documented on the finalize step). Every API call in both steps is best-effort with a `::warning::` fallback, so neither step can fail the job, and `always()` only widens when the last step in the job runs.
- Not validated / out of scope: a live cancelled-run E2E against GitHub Actions (cancellation cannot be triggered deterministically from a PR); the behavior is pinned by the shape and executed-harness tests instead.
- Breaking changes / migration notes: none — the marker, the stage comments, and every API surface are unchanged.

## Linked Issues

None tracked.

<details>
<summary>中文说明</summary>

## 本 PR 做了什么

Triage workflow 在运行开始时会发布一条生命周期状态评论("🔄 Qwen Triage is running"),并由最后的 'Finalize triage status comment' 步骤把同一条 marker 评论翻转为终态。此前两个步骤各自重新执行 marker 评论查找,可能对"本次运行拥有哪条评论"产生分歧(一个可能竞态的选择);且 finalize 的条件 `(success() || failure())` 在取消时(job 级超时或手动取消)永不为真,于是该路径上步骤被跳过,状态评论一直声称运行仍在进行。

本改动通过显式的 id 交接(而非第二次查找)把两个步骤耦合起来:claim 步骤 upsert marker 评论并导出它创建(用 `--jq '.id'` 提取)或复用的确切评论 id,finalize 步骤只 PATCH 这个确切的 id —— finalize 阶段不再重新派生任何内容。同时把 finalize 条件放宽为 `always()` 以覆盖取消,并传入 `${{ job.status }}` 作为 `JOB_STATUS`,从而在既有的"已完成 / 提前结束"文案之外选择专门的"已取消"终态文案;且当 job 本身失败时,不再允许仅凭步骤成功就声称"已完成"。

## 为什么需要

卡住的 "running" 声明会误导所有关注该 issue 的人:运行已经结束,评论却显示仍在进行,而本 lane 没有任何东西会纠正它 —— verify lane 有 publish-verify 专门处理这种情况,而本 lane 只有 finalize 步骤。把条件放宽为 `always()` 覆盖了评论可能保持过期的最后一条路径(取消);让 finalize 以 claim 导出的 id 为准,则彻底消除了两步之间的选择分歧。

## 审阅者测试计划

### 如何验证

运行固定此行为的 workflow shape 测试:`npx vitest run scripts/tests/qwen-triage-workflow.test.js --config ./scripts/tests/vitest.config.ts`(129 个测试)。它断言:claim 步骤把 `comment_id` 导出到 `$GITHUB_OUTPUT` 并用 `--jq '.id'` 提取创建的 id;finalize 步骤从 claim 步骤的输出读取 `STATUS_COMMENT_ID` 并只 PATCH 这个确切的 id、且完全不做评论列举(没有 `--paginate`、没有 `gh api user`);finalize 的 `if:` 条件是 `always() && steps.resolve.outputs.number != ''`;由 `steps.triage.outcome``job.status` 驱动的三态文案选择(已完成 / 已取消 / 提前结束);以及空 id 的 no-op。claim 与 finalize 脚本还会在带故障注入分支的桩 `gh` 上实际执行,断言每种终态 PATCH 的确切 body、最新 marker 复用、legacy marker 复用,以及写失败时导出空 id 且不使步骤失败。

### 证据(前后对比)

N/A —— 仅 workflow 改动,无 TUI 界面。

### 测试环境

|     OS     | 状态 |
| :--------: | :----: |
|  🍏 macOS  |   ⚠️   |
| 🪟 Windows |   ⚠️   |
|  🐧 Linux  ||

### 环境(可选)

N/A —— 仅单元/shape 测试(vitest)。

## 风险与范围

- 主要风险或权衡:finalize 只 PATCH claim 步骤导出的 id。当 claim 的写入瞬时失败 —— 或取消先于 claim 发出 —— 导出的 id 为空,finalize 有意什么都不写,因此即使 comments API 通常会在运行期间恢复,线程也会丢失本次运行的生命周期面包屑;这是可接受的,因为结论仍会落在各阶段评论中,而兜底新发会重新引入本改动要移除的选择逻辑(已在 finalize 步骤中记录)。两个步骤中的每个 API 调用都是尽力而为、失败时 `::warning::` 兜底,因此都不会使 job 失败;`always()` 也只是放宽该 job 最后一个步骤的执行时机。
- 未验证 / 超出范围:针对 GitHub Actions 的真实取消运行 E2E(无法从 PR 确定性地触发取消);行为改由 shape 测试与实际执行装置固定。
- 破坏性变更 / 迁移说明:无 —— marker、各阶段评论以及所有 API 接口均未改变。

## 关联 Issue

无。

</details>

Verification

  • npx vitest run scripts/tests/qwen-triage-workflow.test.js --config ./scripts/tests/vitest.config.ts — 129/129 passed (baseline before the change, after the change, and after restoring the mutant revert)
  • Mutant check (R9-2): removing --jq '.id' from the claim POST arm — suite stayed green BEFORE the pin (finding reproduced) and FAILS on the new assertion AFTER the pin (mutant killed)
  • npm run typecheck — passed
  • npm run lint — passed (touched files are a workflow .yml and a scripts/tests/*.js shape test, outside eslint's .ts,.tsx scope)
  • npm run build — passed
  • YAML parse of .github/workflows/qwen-triage.yml — valid
  • npx prettier --check scripts/tests/qwen-triage-workflow.test.js — passed
  • Integration tests after npm run bundle — not applicable: the touched behavior is workflow YAML plus its shape/executed-harness test, not the bundled CLI
  • npm run generate:settings-schema — not applicable: no settings source changed
中文说明

第 9 轮反馈:两条 inline 发现均以最小改动解决(新增一条测试断言、修正步骤注释)。长期存在的 Critical R8-9(PR 正文不符合模板)本 agent 无法代为应用 —— 下方附了更新后可直接粘贴的正文,供 PR 作者或维护者使用。本轮无需解决与 base 的冲突(--conflict false)。

1. [Suggestion] R9-2 —— claim 的 --jq '.id' 提取从未被任何测试实际执行 —— 已解决。

先复现了报告中的变异体:把 --jq '.id' 从 claim 的 POST 分支删掉后,全部 129 个测试依然通过(桩 gh 对 POST 直接回答已提取好的 id,因此没有任何东西执行该 flag)。采用了审阅者较便宜的方案 (2):在现有 claim 步骤断言旁加一个静态钉桩 —— expect(statusStep).toContain("--jq '.id'") —— 并用注释说明为何已执行装置无法捕获该变异体。加钉桩后重跑同一变异体,套件会在该断言上失败,因此文中描述的失败场景(flag 被删 → 生产环境 COMMENT_ID 捕获完整的 created-comment JSON → $GITHUB_OUTPUT 写入畸形 → finalize 用垃圾 id 发 PATCH → 生命周期评论永久停留在 "running")现在会被捕获。方案 (1) —— 让桩模拟 --jq —— 被有意跳过:它会增大共享装置,却不能捕获任何钉桩捕获不到的东西。

2. [Suggestion] R9-3 —— 空 id 的 no-op 丢掉了本 PR 之前 finalize 的恢复路径 —— 已解决(采用文档方案)。

采用了审阅者的第二个方案:finalize 步骤注释现在记录了这一有意取舍。空 id 有两种成因 —— 取消先于 claim 发出,或 claim 的写入瞬时失败(新发 POST 与 PATCH 两个分支失败时都导出空 id)—— 两种情况下 finalize 都有意什么都不写。注释现在写明:因此 claim 失败时,即使 comments API 在数分钟的 agent 运行期间恢复,线程也不会得到生命周期面包屑;以及为何可接受:结论仍会落在各阶段评论中,而"先列出再新发"的恢复路径会重新引入本 PR 要移除的评论选择逻辑(套件已把 finalize 钉死为完全不做列举:not.toContain('--paginate')not.toContain('gh api user'))。相应的测试注释也对齐为相同的"双成因"表述。带守卫的兜底 POST(方案 1)被否决,因为它重新加回了本改动要移除的列举逻辑,并与上述既有断言冲突。

3. [Critical] R8-9 —— PR 正文必须遵循 PR 模板 —— 本 agent 无法应用;需要维护者/作者操作。

自 triage stage 1a(review 4841776175)以来未变:描述仍使用自由的 Summary/Changes/Verification 标题,且没有 Test Plan 部分。本 agent 没有修改 PR 描述的渠道 —— address-review 流程的 GitHub 写操作仅限于推送提交、发表评论、解决/回复评审线程。早先起草的可直接粘贴正文(issue 评论 5196984782)描述的是上一版"所有权匹配"迭代,已经过时;下方更新后的模板合规正文反映当前的 id 交接设计。应用后请重新运行 @qwen-code /triage,以便 stage 1a 继续。

(更新后的 PR 正文以代码块形式附在上方英文部分的折叠块中,其本身已按模板要求包含完整的中文说明,此处不再重复翻译。)

验证

  • npx vitest run scripts/tests/qwen-triage-workflow.test.js --config ./scripts/tests/vitest.config.ts —— 129/129 通过(改动前基线、改动后、以及变异体还原后均通过)
  • 变异体检查(R9-2):把 --jq '.id' 从 claim 的 POST 分支删掉 —— 加钉桩前套件仍全绿(复现该发现),加钉桩后在新断言上失败(变异体被杀死)
  • npm run typecheck —— 通过
  • npm run lint —— 通过(本次改动的文件是 workflow .ymlscripts/tests/*.js shape 测试,不在 eslint 的 .ts,.tsx 范围内)
  • npm run build —— 通过
  • .github/workflows/qwen-triage.yml 的 YAML 解析 —— 合法
  • npx prettier --check scripts/tests/qwen-triage-workflow.test.js —— 通过
  • npm run bundle 后的集成测试 —— 不适用:本次改动的行为是 workflow YAML 及其 shape/实际执行装置测试,不经由打包后的 CLI
  • npm run generate:settings-schema —— 不适用:未改动任何 settings 源

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

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


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

@wenshao

wenshao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

Scripted assertions: 128 passed · 0 failed · 128 total

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

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

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

Verification report

Verification report — PR #8436 fix(triage): finalize the status comment on cancellation too (follow-up round 3)

Verdict: merge-ready — 128/128 scripted assertions passed, 0 unexpected failures. Verified head: b8ac04ef99c37cab53b28d2c80cbfe55f853f557 (git rev-parse HEAD^2); base tip a8fb97d16f3ce393377a14ad6c45f656b191c718 (HEAD^1). Follow-up to the round that verified 2d14f518b3feb1746e5cb36841c897a53aac13a0; three substantive commits landed since: 4708d1ad (keep finalize flips off live running claims — superseded, see Corrections), 02aad770 (key the finalize on the claim's exported comment id — a redesign replacing the list-and-match finalize), and b8ac04ef (pin the claim's --jq '.id' extraction, document the finalize empty-id loss).

中文摘要
  • 结论merge-ready。128/128 脚本化断言通过,0 个意外失败;无阻塞性发现(3 条 informational)。
  • 上一轮发现状态:R2-F1(双 marker 角落使本 run 的 claim 滞留 "running")已被新设计取代——finalize 不再列评论/按 body 匹配,而是 PATCH claim 步骤导出的确切 id;残留的滞留路径需要"PATCH 已在服务端提交但响应丢失"这一更窄的角(EE5,informational)。R2-F2(timeout→job.status 服务端语义)仍成立(环境限制)。R2-M8(claim 选择器覆盖缺口)已修复——选择器回到 last(newest-wins),且新增的 claim composer 执行测试钉死了该选择(突变 M6 使套件与执行 harness 双红)。
  • A/B 结论:中心论断成立。cancelled 场景 base 跳过 finalize(🔄 永远滞留)、head 翻转自有 id(01-ab-finalize-base-vs-head.png);空 id 时 head 零写入、base 会覆写其他 run 的终态评论(FA6 对照);claim 两臂 body 字节一致,head 额外导出 id,--jq '.id' 经真实 jq 执行验证(02-ab-claim-id-export.png);12 组合措辞真值表全对(03-edges-truth-table.png)。
  • 测试非空转:7 个点突变全部杀死(套件;其中 5 个同时被执行 harness 杀死),正向对照 M2 如期红;树逐字节复原。
  • 门禁:vitest 129/129、node --test 56/56、actionlint 1.7.12 head/base 双零发现(含 liveness 探针)、shellcheck/bash -n 四个抽取脚本全清(含 liveness 探针)、cache-key 引号翻转解析值恒等(9/9);yamllint 仍无法安装(已重证)。
  • 未覆盖:逐 commit 归因(depth-2 仅可达 1/25 commit);无真实已发评论校准 replay;GHA 服务端 timeout-minutesjob.status 语义;并发交错为推理+单元格证据,未在真实 API 上重放。

Previous-round finding status (re-measured at the new head)

# finding (round 2 at 2d14f518) severity status at b8ac04ef
1 Double-marker corner: a stranded older "running" own claim behind a newer marker was only healed by the claim step's oldest-running reclaim informational superseded by redesign — re-measured, not diffed: the whole list-and-match finalize and the reclaim selector were removed in 02aad770; finalize now PATCHes exactly the id the claim exported, so the old creation path (finalize skipping on a foreign match) is gone. Residual: a stranded running comment can now only arise from an export loss (write committed server-side, response lost — CA6 measures the plain-failure half, which exports empty correctly), and it persists only if a newer marker later appears (cell EE5: claim picks newest 45, orphan 44 untouched). Narrower than round-1's shape, and the base tree had the same newest-wins selection (no reclaim either), so not a regression vs base. Reported as Finding 1.
2 timeout-minutesjob.status semantics bounded, not measured (GitHub-server property) informational stands — still unobservable in-container. The wording branch handles both readings (job timeout reads as failure → "ended early"; cancel reads as cancelled → "was cancelled"), and the 12-combo truth table (EE7) covers every (TRIAGE_OUTCOME, JOB_STATUS) pair.
3 M8 coverage gap: claim own-marker lastfirst survived the suite (no two-own-marker fixture) informational fixed — the selector is now the plain newest-wins last, and the new executed claim composer test pins it: mutant M6 (lastfirst) turns the suite RED (1 test) and my A/B harness RED (CA2).

Declined rows: none in round 2. Round-2 Finding 1 (unreadable-list asymmetry between claim and finalize) is also superseded: finalize no longer lists at all (FA8: a list outage cannot affect it — zero GET calls by construction, asserted in EE3), and the claim side's || EXISTING_ID='' fresh-POST fallback is byte-parity with base (CA5) and now converges because each run finalizes its own exported id.

Scope

Central claim: the lifecycle comment always reaches a terminal state because the two steps are coupled by id, not by re-selection: (a) the claim step exports the exact comment id it wrote (comment_id — reused id on PATCH, --jq '.id'-extracted created id on POST, empty on any write failure); (b) Finalize triage status comment runs under always() && number != '' (covering cancellation) and PATCHes exactly that id — never lists, never matches bodies, no-ops on an empty id; (c) the wording is correct for every terminal (TRIAGE_OUTCOME, JOB_STATUS) combination, and "finished" never appears on a failed job.

Secondary claims: (1) every write arm survives a failing API under GitHub's real -eo pipefail (exit 0 + warning), and a failed claim write exports an empty id rather than a guess; (2) the two other hunks in the diff — the npm-cache key: quote-style flips in tmux-testing and verify — are semantic no-ops.

A/B result — finalize step (central claim a–c)

Harness ab-finalize.mjs extracts each arm's if: + run: from the YAML via the yaml package, evaluates the if: under GitHub status-function semantics (success() and failure() are both false on cancellation — that is exactly the gap the PR closes), and executes the bash under bash --noprofile --norc -eo pipefail with a recording gh stub + real jq. Witness: 01-ab-finalize-base-vs-head.png. Fixture: this run's claim id 43, RUN_URL=…/runs/77.

cell base (list-and-flip-newest, success()‖failure()) head (PATCH exported id, always())
FA1 success/success, own 43 PATCH 43 "finished" PATCH 43 "finished", body byte-identical, exactly one call total
FA2 failure/failure, own 43 PATCH 43 "early" (half-width ZH comma) PATCH 43 "early" (full-width comma — the declared fix; bodies identical after normalizing that one byte)
FA3 cancelled/cancelled (the bug) SKIPPED — 🔄 claim stranded forever PATCH 43 "was cancelled / 已取消"
FA4 cancelled job, triage step success SKIPPED PATCH 43 "finished" (triage success precedes a late cancel)
FA5 failed job, triage step success ('Check triage response' exits 1) PATCH 43 "finished" — points at stage comments that never posted PATCH 43 "ended early"
FA6 empty id; foreign tombstone 44 (runs/55) PATCHes 44 — clobbers the previous run's terminal wording zero writes + "claimed no status comment" diagnostic
FA7 empty id; empty thread POSTs a fresh "early" comment zero writes (accepted breadcrumb loss, documented in the step comment)
FA8 list API down, own 43 conflates unreadable with empty → POSTs fresh PATCHes 43 (makes no list call at all — immune)
FA9 number='' skipped skipped (guard parity)
FA10 two pages, own on page 2, cancelled skipped (would have needed the pagination idiom) PATCHes 43 with one call

40/40 assertions, including the control cells asserting the base arm misbehaves exactly as predicted (FA3 skip, FA5 false "finished", FA6 clobber, FA8 conflation).

A/B result — claim step (central claim a, secondary 1)

ab-claim.mjs, same mechanics. The stub answers a POST with a JSON payload {"id": N} and applies the caller's --jq filter with real jq (as gh does client-side) — one fidelity level above the PR's own suite, whose stub echoes a bare id and therefore cannot exercise the --jq '.id' flag the last commit pinned statically. Witness: 02-ab-claim-id-export.png.

cell base head
CA1 empty thread POST body POST byte-identical body; --jq .id in raw args; exports comment_id=7777 (extraction executed through real jq)
CA2 tombstones 42+44 PATCH newest 44 PATCH 44, exports 44 (shared-slot semantics unchanged)
CA3 legacy marker 41 PATCH 41 PATCH 41, exports 41 (single-slot healing kept)
CA4 forged non-bot markers POST fresh (author filter) POST fresh, exports 7777
CA5 unreadable list POST fresh (‖ EXISTING_ID='') POST fresh, exports 7777 (parity with base)
CA6 POST-fail / PATCH-fail exit 0 + warning both arms exit 0 + warning, exports EMPTY id both arms (finalize will no-op, never a guessed id)
CA7 bot identity failure exit 0, warn, no writes same, and exports nothing (finalize sees empty id)

34/34 assertions.

Head-only edge matrix + wording truth table

edges.mjs. Witness: 03-edges-truth-table.png.

cell result
EE1 the jq-null corner measured: a POST payload without .id would make the pinned --jq '.id' emit literal null (real jq), which finalize would PATCH as comments/null — still exit 0 (best-effort holds). Unreachable on the real comments API (201 always carries id); see Finding 2
EE2 STATUS_COMMENT_ID fully unset (set -u safe via ${…:-}): exit 0, zero writes, diagnostic
EE3 finalize makes exactly one call (the PATCH); zero list/user calls — the no-re-selection property, enforced
EE4 exactly one comment_id= line in $GITHUB_OUTPUT per claim run
EE5 stranded-corner measurement: fixture [running 44 (older), tombstone 45] → claim PATCHes newest 45; orphan 44 untouched (see Finding 1)
EE6 finalize body starts with the lifecycle marker and keeps the run link
EE7 full truth table: 4 outcomes × 3 job statuses = 12 combos, each asserting the exact expected wording and the absence of the other two; plus a dedicated sweep proving no combo with JOB_STATUS=failure ever says "finished"

22/22 assertions.

Mutation matrix (vacuity of the new tests)

Seven point mutants applied in place (byte-exact, single-occurrence asserted, restored after each, tree verified byte-identical at the end); per mutant the vitest suite (129 tests) re-run, and for the five mutants targeting behavior my harnesses execute, the matching harness too. Unmutated controls: suite 129/129 green; harnesses 96/96 green. Witness: 04-mutation-matrix.png.

mutant suite executed harness
M1 claim POST arm: drop --jq '.id' RED (1 test — the static pin) RED (ab-claim: export becomes the raw payload, not 7777)
M2 swap cancelled↔early EN wording (positive control) RED (5 tests)
M3 drop the JOB_STATUS != failure gate on "finished" RED (2 tests)
M4 remove the empty-id no-op guard RED (2 tests) RED (ab-finalize FA6: writes appear where none may)
M5 claim exports the reused id even when the PATCH fails RED (1 test) RED (ab-claim CA6: stale 45 exported)
M6 claim selector lastfirst RED (1 test) RED (ab-claim CA2: PATCHes 42, exports 42)
M7 finalize always()success()‖failure() RED (1 test) RED (ab-finalize: condition shape + cancelled cell)

No survivors; no mutant regressed. M1 is the attribution the last commit claims: the executed harness catches the dropped flag behaviorally, where the suite pins it only statically (the suite's stub bypasses --jq).

Secondary claim 2 — cache-key quote flips

cache-key-parity.mjs: the PR also flips two key: lines (tmux-testing, verify jobs) between single-quoted-with-escaped-quotes and double-quoted YAML styles. The PARSED values are byte-identical across the flip (npm-ci-${{ hashFiles('package-lock.json') }}), restore-keys and the action refs unchanged — cache identity is untouched. 9/9 assertions. Both merge parents carried the different styles, so the flip is part of what lands; semantically a no-op.

Corrections

  • Commit 4708d1ad describes a change that is not in the final diff. Its message says qwen-triage-finalize.yml's update_status (the third writer of the lifecycle marker) was taught to "prefer the newest marker that is not a live running claim". Verified against the aggregate diff: git diff HEAD^1..HEAD --stat -- .github/workflows/qwen-triage-finalize.yml is empty — update_status still selects | last | unconditionally (lines 326–336 at head). This is a correction to the commit description, not a request to change the code: the harm that commit set out to fix (finalize.yml erasing the run URL the OLD finalize keyed on → misclassification as foreign → second post + permanent strand) no longer exists, because 02aad770 removed body/URL-keyed selection entirely — the triage finalize PATCHes the exported id, so an update_status overwrite of a live claim's body can no longer break the flip. What remains is the pre-existing, base-identical cosmetic overwrite (see Finding 3).

Findings (non-blocking)

  1. Stranded "running" comments can still persist in one narrow corner (informational). Creation requires the claim's PATCH to commit server-side but lose its response (so comment_id exports empty while the comment carries this run's running body), AND a newer marker to appear later (a list-failure fresh POST or a double-POST race). Measured in EE5: with [running 44 older, tombstone 45], the claim reuses newest 45 and never touches 44 — the round-1/2 oldest-running reclaim was deliberately removed ("a list-then-POST recovery here would reintroduce exactly the selection this coupling removes"). The plain write-failure half is handled and documented (CA6 exports empty; the YAML comment names it); the response-loss half is not named there. Not a regression vs base (base also selected newest-wins and had no reclaim), strictly narrower than the round-1 shape, and every run's own comment is finalized by id. No action required; optionally name the response-loss half in the step comment.
  2. jq-null corner on the POST arm (informational). If a POST ever returned 200 without an id field, --jq '.id' emits literal null, which [ -n ] accepts, exporting comment_id=null; finalize would PATCH issues/comments/null, gh would 404, and the best-effort guard would warn and exit 0 (measured EE1a/EE1b). Unreachable on the real comments API (a 201 always carries id; error statuses exit non-zero → || COMMENT_ID=''). No action required.
  3. Shared-slot interleavings still transiently mislabel during overlapping runs; the advertised live-claim guard is absent (informational, pre-existing). The marker comment is a shared slot: run B's claim overwrites run A's running body, and A's later finalize flips the slot while B is live (the slot converges when B finalizes — measured convergent for all wording combos, EE7). This is base-identical and accepted by the slot design. Separately, see Corrections: the qwen-triage-finalize.yml live-claim guard from 4708d1ad did not land, so update_status can still cosmetically overwrite a live claim until that run finalizes. No action required.

Not covered

  • Uncalibrated replay — no GitHub token and no real lifecycle comment available locally, so the harnesses were not byte-calibrated against a production-emitted artifact (same as rounds 1–2). Partial corroboration: the claim bodies the harnesses flip are the exact bytes the head claim step composes (CA1 capture), and actionlint fully parses both trees' YAML.
  • Per-commit attribution — depth-2 checkout: git rev-list HEAD^1..HEAD^2 returns 1 commit (shallow boundary) against 25 in the metadata snapshot, so the three substantive commits since round 2 were verified via cells aimed at their claims (02aad770 → FA6/FA8/EE3 coupling cells; b8ac04ef → M1/CA1 extraction cells; 4708d1ad → Corrections finding), not isolated per-commit builds. The aggregate HEAD^1..HEAD diff (2 files) is what was verified.
  • yamllint gate — environmental, re-proven this round: pip3: Permission denied in scripts/lint.js --setup (quoted in logs/lint-setup.log). YAML validity still covered by actionlint's full parse on both trees and every harness's yaml parse.
  • GHA server-side semanticstimeout-minutesjob.status value, and the job.status context availability inside env: (previous-finding 2, stands). actionlint validates the context usage and accepts it; the lane has run with this env since round 2.
  • Live concurrency replay against the real API — the interleavings in Finding 3 are reasoned + cell-measured, not replayed on GitHub.
  • Full-repo eslint/lint — covered by the PR's own CI; the only non-workflow file changed is a test file.
  • The PR description's test count ("120/120") is stale: the suite at head is 129 tests (it grew with the later commits). No significance.

Methodology

Environment: the CI verify container (node:22-bookworm, node v22.23.2, jq 1.6; actionlint 1.7.12 + shellcheck 0.11.0 installed pinned via scripts/lint.js --setup into /tmp/qwen-code-linters/local-*; yamllint uninstallable). The PR touches only workflow YAML and one test file — no compiled packages and no dependency changes (package.json/lockfile untouched), so the A/B "builds" are the two trees' YAML: head .github/workflows/qwen-triage.yml vs the tmp/base-tree worktree at HEAD^1, whose file sha256 was asserted equal to git show HEAD^1 (909ba7fd…). The code under test is the verbatim-extracted bash executed under GitHub's exact shell flags (bash --noprofile --norc -eo pipefail) with real jq and a recording gh stub that applies --jq filters like gh does (no stub of the code under test itself); the if: conditions are evaluated under GitHub status-function semantics by evalIf, which refuses unknown shapes. Harnesses (in this dir): ab-finalize.mjs (40), ab-claim.mjs (34), edges.mjs (22), cache-key-parity.mjs (9), mutate.mjs (7 mutants + restore = 8 checks), gate runs incl. two liveness probes (15). Raw per-run logs in logs/; evidence PNGs via scripts/verify-capture.mjs. Assertion counts: 40+34+22+9+8+15 = 128/128.

Evidence images

01-ab-finalize-base-vs-head

02-ab-claim-id-export

03-edges-truth-table

04-mutation-matrix

05-gates

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

Qwen Code · sandboxed verification

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

The Stage 1a template gate still fails at b8ac04ef99c37cab53b28d2c80cbfe55f853f557 — the PR body is unchanged since the last run, so the gate result is identical and no new request-changes review was stacked (existing ones already gate the PR; the latest, #4879739695, sits on this exact commit). The stage=1a comment above was updated in place. Action needed: the author or a maintainer edits the PR description to follow the template, then re-runs @qwen-code /triage. View workflow run.

Triage 重跑完成,未产生新评审。

Stage 1a 模板关卡在 b8ac04ef99c37cab53b28d2c80cbfe55f853f557 上仍未通过——PR 正文自上次运行以来没有变化,关卡结果完全相同,因此未叠加新的 request-changes 评审(已有评审正在把关该 PR,最新的 #4879739695 就提交于这个 commit 之上)。上方 stage=1a 评论已原地更新。需要的动作:作者或 maintainer 按模板修改 PR 描述,然后重新运行 @qwen-code /triage查看工作流运行

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not reviewed: test-efficacy probe — the probe kit could not collect scripts/tests (workspace-only discovery; its 'unreachable' claim is disproven by direct execution: 129/129 pass here and the suite is gated in CI via test:ci → test:scripts), so revert/mutant/hunk probing did not run; compensated by manually executed mutants (four mutants, all caught by the new tests).

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

[Critical] R8-9 Existing CHANGES_REQUESTED (triage stage 1a, review 4841776175, 2026-08-03): the PR body must follow the PR template — still stands at the reviewed commit: the description still uses freeform Summary/Changes/Verification headings and has no Test Plan section (test-plan check: 'the PR description has no Test Plan section'). Subject is the PR description, not the code: an updated paste-ready template-compliant body reflecting the current id-handoff design was drafted in the thread (issue comment 5211486662); the author or a maintainer needs to apply it and re-run @qwen-code /triage.

中文说明

未审查:test-efficacy probe — the probe kit could not collect scripts/tests (workspace-only discovery; its 'unreachable' claim is disproven by direct execution: 129/129 pass here and the suite is gated in CI via test:ci → test:scripts), so revert/mutant/hunk probing did not run; compensated by manually executed mutants (four mutants, all caught by the new tests)。

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

[Critical] R8-9 Existing CHANGES_REQUESTED (triage stage 1a, review 4841776175, 2026-08-03): the PR body must follow the PR template — still stands at the reviewed commit: the description still uses freeform Summary/Changes/Verification headings and has no Test Plan section (test-plan check: 'the PR description has no Test Plan section'). Subject is the PR description, not the code: an updated paste-ready template-compliant body reflecting the current id-handoff design was drafted in the thread (issue comment 5211486662); the author or a maintainer needs to apply it and re-run @qwen-code /triage.

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

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, no blockers. The id-passing design is a real improvement: the claim step exports the exact comment id it wrote, and finalize PATCHes precisely that id — removing the old list-then-match selection that could race or clobber another run's comment. always() (not success||failure) correctly covers cancellation; JOB_STATUS gating prevents a red job from saying 'finished'; empty id is a no-op (never a fresh post or lookup that could overwrite a prior run's terminal wording); a failing PATCH warns instead of turning the job red under -eo pipefail. The executed-harness tests run the real step scripts under GitHub's exact bash flags with failure injection — thorough. Bonus fix: the npm cache key YAML quoting. No P0/P1.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

No code changes this round: the code itself is fully verified and approved, and the only open item (R8-9) targets the PR description, which this lane cannot edit. Point by point:

1. [Critical] R8-9 — the PR body must follow the PR template — VALID; NOT APPLICABLE BY THIS AGENT; author/maintainer action required.

Unchanged since triage stage 1a (review 4841776175): the description still uses freeform Summary/Changes/Verification headings and has no Test Plan section. This agent has no channel to edit the PR description — the address-review lane's GitHub writes are limited to pushing commits, posting comments, and resolving/replying to review threads (verified against the workflow: there is no PR-body edit path). The paste-ready template-compliant body in the collapsed block below supersedes the draft in issue comment 5211486662: it folds in @wenshao's round-5 §5 correction (the two npm cache-key lines are formatter-required fixups of a violation main currently has, not unrelated churn), states explicitly that finalize never POSTs (a deleted status comment is not resurrected), names the shared-slot residual risk, and cites the current 129-test count. After applying it, please re-run @qwen-code /triage so stage 1a can continue.

2. The review's "Not reviewed" / "Not linted" notes — INFORMATIONAL, no action.

The test-efficacy probe's workspace-only discovery could not collect scripts/tests; its "unreachable" claim is disproven by direct execution — the suite was re-run fresh at the current head this round (129/129 passed, see Local checks) — and it is gated in CI via test:ci → test:scripts. The four manually executed mutants were all caught. The actionlint embedded-shell source-mapping gap on .github/workflows/qwen-triage.yml is a tool limitation the review itself marks as not a blocker.

3. @wenshao round-5 verification (issue comment) — ACKNOWLEDGED, no code action.

The "ready to merge on the code" verdict is backed by the 15-scenario × 2-lane matrix (6 FIXED / 5 CONTROL / 3 ROBUST / 0 regressions), 8/8 mutant kills, and a provably contained blast radius (7 of 361 parsed leaf values differ). Its single open item — the body describing only about half the diff, quoting wording that is not in the diff, and citing a stale 120/120 count — is exactly R8-9, addressed by the updated body below, which covers the id handoff (id: 'status' + the comment_id output), the JOB_STATUS != 'failure' guard, the no-POST finalize, the two formatter fixups, and the 129-test count.

4. @wenshao §5 correction (issue comment) — ACKNOWLEDGED, no code change, folded into the body draft.

The two npm-ci- cache-key quote-style flips are required by the formatter: main fails npx prettier --check on that file, prettier --write reproduces the PR's two lines byte-for-byte, and scripts/lint.js --prettier runs prettier --write . (a fixer, so its exit code proves nothing about the input — confirmed at scripts/lint.js:208). The lines are already in the diff and their parsed values are byte-identical; the correction only changes their characterization, which the updated body now states. At head, npx prettier --check passes on both changed files (see Local checks).

5. @qwen-code-ci-bot sandboxed verification (issue comment) — ACKNOWLEDGED, no action.

✅ merge-ready, 128/128 scripted assertions at head b8ac04ef99. Its three informational findings are all explicitly "no action required": the stranded-comment corner now requires a PATCH committed server-side with a lost response (narrower than any earlier shape, and the empty-id export half is documented on the finalize step); the jq-null corner is unreachable on the real comments API; the shared-slot transient mislabel self-heals when the superseding run finalizes (now also named in the body's Risk & Scope).

6. @yiliang114 APPROVED (review 4879802414, 2026-08-07T03:52:52Z) — ACKNOWLEDGED.

Landed after the round-10 review and carries no findings: "LGTM, no blockers … No P0/P1." No action needed.

Local checks (unchanged head b8ac04ef99; no commit this round)

  • npx vitest run scripts/tests/qwen-triage-workflow.test.js --config ./scripts/tests/vitest.config.ts — 129/129 passed
  • npx prettier --check .github/workflows/qwen-triage.yml scripts/tests/qwen-triage-workflow.test.js — passed
  • git status --porcelain — clean; HEAD is the verified commit, untouched
Updated paste-ready PR body (template-compliant, current id-handoff design; supersedes the draft in issue comment 5211486662)
## What this PR does

The triage workflow posts a lifecycle status comment ("🔄 Qwen Triage is running") when a triage run starts, and a final 'Finalize triage status comment' step flips that same marker comment into a terminal state. Previously each step re-ran its own marker-comment lookup, so the two could disagree about which comment this run owned — a selection that could race — and the finalize gate `(success() || failure())` was never true on cancellation (a job-level timeout or a manual cancel), so on that path the step was skipped and the status comment kept claiming the run was still in progress.

This change couples the two steps through an explicit id handoff instead of a second lookup: the claim step (`id: 'status'`) upserts the marker comment and exports to `$GITHUB_OUTPUT` the exact comment id it created (extracted with `--jq '.id'`) or reused, and the finalize step PATCHes exactly that id — nothing is re-derived at finalize time, no comment listing happens there, and finalize never POSTs, so a status comment deleted mid-run is not resurrected. The finalize gate is widened to `always()` so cancellation is covered too, and `${{ job.status }}` is passed in as `JOB_STATUS` so a dedicated cancelled terminal wording is selected alongside the existing finished / ended-early wordings, with a step-level success no longer allowed to claim "finished" when the job itself failed. The diff also carries two prettier-required quote-style fixups on the npm cache keys whose parsed values are byte-identical (`main` currently fails `prettier --check` on that file), and a full-width-comma fix in one Chinese wording string.

## Why it's needed

A stuck "running" claim misleads everyone watching the issue: the run ended, but the comment says it is still in progress, and nothing in this lane ever corrects it — the verify lane has publish-verify for exactly this case, while this lane only has the finalize step. Widening the gate to `always()` covers the one remaining path (cancellation) on which the comment could be left stale, and keying finalize on the claim's exported id removes the two-step selection disagreement entirely.

## Reviewer Test Plan

### How to verify

Run the workflow shape test that pins this behavior: `npx vitest run scripts/tests/qwen-triage-workflow.test.js --config ./scripts/tests/vitest.config.ts` (129 tests). It asserts that the claim step exports `comment_id` to `$GITHUB_OUTPUT` and extracts the created id with `--jq '.id'`; that the finalize step reads `STATUS_COMMENT_ID` from the claim step's output and PATCHes exactly that id while doing no comment listing at all (no `--paginate`, no `gh api user`); that the finalize `if:` condition is `always() && steps.resolve.outputs.number != ''`; the three-way wording selection (finished / cancelled / ended-early) driven by `steps.triage.outcome` and `job.status`; and the empty-id no-op. The claim and finalize scripts are additionally executed against a stubbed `gh` with failure-injection arms, asserting the exact body PATCHed for every terminal state, newest-marker reuse, legacy-marker reuse, and that a failed write exports an empty id without failing the step. Formatting gate: `npx prettier --check` passes on both changed files (note `node scripts/lint.js --prettier` runs `prettier --write .` — it is a fixer, not a check).

### Evidence (Before & After)

N/A — workflow-only change, no TUI surface.

### Tested on

|     OS     | Status |
| :--------: | :----: |
|  🍏 macOS  |   ⚠️   |
| 🪟 Windows |   ⚠️   |
|  🐧 Linux  ||

### Environment (optional)

N/A — unit/shape tests only (vitest).

## Risk & Scope

- Main risk or tradeoff: finalize PATCHes exactly the id the claim step exported. When the claim's write fails transiently — or a cancel lands before the claim posts — the exported id is empty and finalize deliberately writes nothing, so the thread loses this run's lifecycle breadcrumb even though the comments API typically recovers during the run; accepted because the verdict still lands in the stage comments, and a list-then-POST recovery would reintroduce exactly the selection this coupling removes (documented on the finalize step). Every API call in both steps is best-effort with a `::warning::` fallback, so neither step can fail the job, and `always()` only widens when the job's last step runs. Residual, accepted: the marker is a shared slot, so a cancelled run's late finalize can transiently mislabel a slot already re-claimed by a newer run; the newer run's own finalize corrects it (bounded by that run's duration, no duplicate comments), which is strictly better than the pre-PR behavior of stranding a contradicting comment.
- Not validated / out of scope: a live cancelled-run E2E against GitHub Actions (cancellation cannot be triggered deterministically from a PR) — the behavior is pinned by the shape and executed-harness tests instead; a `publish-triage-status` job shaped like publish-verify to survive whole-runner loss is a follow-up, not this PR.
- Breaking changes / migration notes: none — the marker, the stage comments, and every API surface are unchanged.

## Linked Issues

None tracked.

<details>
<summary>中文说明</summary>

## 本 PR 做了什么

Triage workflow 在运行开始时会发布一条生命周期状态评论("🔄 Qwen Triage is running"),并由最后的 'Finalize triage status comment' 步骤把同一条 marker 评论翻转为终态。此前两个步骤各自重新执行 marker 评论查找,可能对"本次运行拥有哪条评论"产生分歧(一个可能竞态的选择);且 finalize 的条件 `(success() || failure())` 在取消时(job 级超时或手动取消)永不为真,于是该路径上步骤被跳过,状态评论一直声称运行仍在进行。

本改动通过显式的 id 交接(而非第二次查找)把两个步骤耦合起来:claim 步骤(`id: 'status'`)upsert marker 评论,并把确切评论 id 导出到 `$GITHUB_OUTPUT` —— 创建时用 `--jq '.id'` 提取,复用时直接导出;finalize 步骤只 PATCH 这个确切的 id —— finalize 阶段不再重新派生任何内容、不做任何评论列举、且永远不会 POST,因此运行中途被删除的状态评论不会被重新创建。同时把 finalize 条件放宽为 `always()` 以覆盖取消,并传入 `${{ job.status }}` 作为 `JOB_STATUS`,从而在既有的"已完成 / 提前结束"文案之外选择专门的"已取消"终态文案;且当 job 本身失败时,不再允许仅凭步骤成功就声称"已完成"。diff 还顺带包含两处由 prettier 要求的 npm 缓存 key 引号风格修正(解析后的值逐字节相同;`main` 目前在该文件上 `prettier --check` 不通过),以及一处中文文案的全角逗号修正。

## 为什么需要

卡住的 "running" 声明会误导所有关注该 issue 的人:运行已经结束,评论却显示仍在进行,而本 lane 没有任何东西会纠正它 —— verify lane 有 publish-verify 专门处理这种情况,而本 lane 只有 finalize 步骤。把条件放宽为 `always()` 覆盖了评论可能保持过期的最后一条路径(取消);让 finalize 以 claim 导出的 id 为准,则彻底消除了两步之间的选择分歧。

## 审阅者测试计划

### 如何验证

运行固定此行为的 workflow shape 测试:`npx vitest run scripts/tests/qwen-triage-workflow.test.js --config ./scripts/tests/vitest.config.ts`(129 个测试)。它断言:claim 步骤把 `comment_id` 导出到 `$GITHUB_OUTPUT` 并用 `--jq '.id'` 提取创建的 id;finalize 步骤从 claim 步骤的输出读取 `STATUS_COMMENT_ID` 并只 PATCH 这个确切的 id、且完全不做评论列举(没有 `--paginate`、没有 `gh api user`);finalize 的 `if:` 条件是 `always() && steps.resolve.outputs.number != ''`;由 `steps.triage.outcome` 和 `job.status` 驱动的三态文案选择(已完成 / 已取消 / 提前结束);以及空 id 的 no-op。claim 与 finalize 脚本还会在带故障注入分支的桩 `gh` 上实际执行,断言每种终态 PATCH 的确切 body、最新 marker 复用、legacy marker 复用,以及写失败时导出空 id 且不使步骤失败。格式化门禁:`npx prettier --check` 在两个改动文件上均通过(注意 `node scripts/lint.js --prettier` 执行的是 `prettier --write .` —— 是修复器而非检查器)。

### 证据(前后对比)

N/A —— 仅 workflow 改动,无 TUI 界面。

### 测试环境

|     OS     | 状态 |
| :--------: | :----: |
|  🍏 macOS  |   ⚠️   |
| 🪟 Windows |   ⚠️   |
|  🐧 Linux  ||

### 环境(可选)

N/A —— 仅单元/shape 测试(vitest)。

## 风险与范围

- 主要风险或权衡:finalize 只 PATCH claim 步骤导出的 id。当 claim 的写入瞬时失败 —— 或取消先于 claim 发出 —— 导出的 id 为空,finalize 有意什么都不写,因此即使 comments API 通常会在运行期间恢复,线程也会丢失本次运行的生命周期面包屑;这是可接受的,因为结论仍会落在各阶段评论中,而"先列出再新发"的恢复路径会重新引入本耦合要移除的选择逻辑(已在 finalize 步骤中记录)。两个步骤中的每个 API 调用都是尽力而为、失败时 `::warning::` 兜底,因此都不会使 job 失败;`always()` 也只是放宽该 job 最后一个步骤的执行时机。已接受的残余风险:marker 是共享槽位,被取消 run 的迟到 finalize 可能瞬时错标已被新 run 重新占用的槽位;新 run 自己的 finalize 会纠正它(以该 run 的执行时间为上限、不会产生重复评论),这严格好于本 PR 之前"永久滞留一条自相矛盾评论"的行为。
- 未验证 / 超出范围:针对 GitHub Actions 的真实取消运行 E2E(无法从 PR 确定性地触发取消)—— 行为改由 shape 测试与实际执行装置固定;形如 publish-verify 的 `publish-triage-status` job 以扛住整台 runner 丢失,属于后续工作,不属于本 PR。
- 破坏性变更 / 迁移说明:无 —— marker、各阶段评论以及所有 API 接口均未改变。

## 关联 Issue

无。

</details>
中文说明

本轮无代码改动:代码本身已充分验证并获批准,唯一未关闭的事项(R8-9)针对的是 PR 描述,而本 lane 无法编辑它。逐点说明如下:

1. [Critical] R8-9 —— PR 正文必须遵循 PR 模板 —— 成立;本 agent 无法代为应用;需要作者/维护者操作。

自 triage stage 1a(review 4841776175)以来未变:描述仍使用自由的 Summary/Changes/Verification 标题,且没有 Test Plan 部分。本 agent 没有修改 PR 描述的渠道 —— address-review 流程的 GitHub 写操作仅限于推送提交、发表评论、解决/回复评审线程(已对照 workflow 核实:不存在修改 PR 正文的路径)。下方折叠块中可直接粘贴的模板合规正文取代 issue 评论 5211486662 中的草稿:并入了 @wenshao 第 5 轮报告第 5 节的更正(两行 npm 缓存 key 是格式化工具要求的修正,修复的是 main 当前存在的违规,而非无关改动),明确写出 finalize 永不 POST(被删除的状态评论不会被重新创建),点名共享槽位的残余风险,并引用当前的 129 个测试。应用后请重新运行 @qwen-code /triage,以便 stage 1a 继续。

2. 该 review 的 "Not reviewed" / "Not linted" 说明 —— 信息性,无需处理。

test-efficacy probe 的 workspace-only 发现机制无法收集 scripts/tests;其"不可达"的说法已被直接执行证伪 —— 本轮已在当前 head 上重新运行该套件(129/129 通过,见"本地检查")—— 且该套件在 CI 中由 test:ci → test:scripts 门禁覆盖。四个人工执行的变异体全部被拦截。.github/workflows/qwen-triage.yml 上的 actionlint embedded-shell source-mapping 缺口是工具限制,review 本身也标注其为非阻断。

3. @wenshao 第 5 轮验证(issue 评论)—— 已知悉,无代码操作。

"代码层面可以合入"的结论有 15 场景 × 2 车道矩阵(6 FIXED / 5 CONTROL / 3 ROBUST / 0 回归)、8/8 变异体拦截、以及可证明收敛的影响面(361 个解析叶子值中仅 7 个不同)支撑。其唯一未关闭事项 —— 正文只描述了约一半 diff、引用了 diff 中不存在的文案、以及过时的 120/120 计数 —— 正是 R8-9,由下方更新后的正文解决:覆盖 id 交接(id: 'status' + comment_id 输出)、JOB_STATUS != 'failure' 护栏、不再 POST 的 finalize、两处格式化修正,以及 129 个测试的当前计数。

4. @wenshao 第 5 节更正(issue 评论)—— 已知悉,无代码改动,已并入正文草稿。

两处 npm-ci- 缓存 key 的引号风格翻转是格式化工具的要求:mainnpx prettier --check 对该文件不通过,prettier --write 产生的两行与 PR 逐字节一致,且 scripts/lint.js --prettier 执行的是 prettier --write .(修复器,其退出码无法说明输入是否合规 —— 已在 scripts/lint.js:208 确认)。这两行已在 diff 中,解析后的值逐字节相同;更正只改变其定性,更新后的正文已按此表述。当前 head 上 npx prettier --check 对两个改动文件均通过(见"本地检查")。

5. @qwen-code-ci-bot 沙箱验证(issue 评论)—— 已知悉,无需处理。

在 head b8ac04ef99 上 ✅ 可合入,128/128 脚本化断言。其三条 informational 发现均明确标注"无需处理":滞留评论的角落现在需要"PATCH 已在服务端提交但响应丢失"(比此前任何形态都更窄,且空 id 导出的一半已在 finalize 步骤记录);jq-null 角落在真实 comments API 上不可达;共享槽位的瞬时错标会在接替 run 完成 finalize 时自愈(现也在正文的"风险与范围"中点名)。

6. @yiliang114 APPROVED(review 4879802414,2026-08-07T03:52:52Z)—— 已知悉。

晚于第 10 轮 review 到达,且不包含任何发现:"LGTM, no blockers … No P0/P1"。无需处理。

本地检查(head 保持 b8ac04ef99 不变;本轮未提交)

  • npx vitest run scripts/tests/qwen-triage-workflow.test.js --config ./scripts/tests/vitest.config.ts —— 129/129 通过
  • npx prettier --check .github/workflows/qwen-triage.yml scripts/tests/qwen-triage-workflow.test.js —— 通过
  • git status --porcelain —— 干净;HEAD 为已验证的提交,未变动

(更新后的 PR 正文以上方折叠块中的代码块形式给出,其本身已按模板要求包含完整的中文说明,此处不再重复翻译。)

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


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

@wenshao

wenshao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

Scripted assertions: 144 passed · 0 failed · 144 total

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

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

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

Verification report

Verification report — PR #8436 fix(triage): finalize the status comment on cancellation too (follow-up round 4)

Verdict: merge-ready — 144/144 scripted assertions passed, 0 unexpected failures. Verified head: b8ac04ef99c37cab53b28d2c80cbfe55f853f557 (git rev-parse HEAD^2); base tip a8fb97d16f3ce393377a14ad6c45f656b191c718 (HEAD^1). This is the same head and base round 3 verified — the lane re-ran without new commits — so every carried measurement was re-executed from scratch this round (fresh harnesses, new cell runs, new mutation matrix) rather than diffed from the old report, and an input-closure check proves the inputs identical (base YAML sha256 909ba7fd… == round-3's cited hash == git show HEAD^1). New this round: main has advanced past the verified base (baseRefOid is now 95655daf), so the merge itself was verified against the current main tip — see "Merge-staleness check".

中文摘要
  • 结论merge-ready。144/144 脚本化断言通过,0 个意外失败;无新发现(沿袭 3 条 informational,均已重测)。
  • 与上一轮的关系:head/base 与第 3 轮完全相同(无新提交),本轮未沿用旧数字,而是重建 harness 全部重跑;base YAML 哈希与第 3 轮引用值一致,输入闭包同一。
  • A/B 结论:中心论断成立。cancelled 场景 base 的 (success() || failure()) 为假 → 步骤被跳过、🔄 claim 永远滞留;head 在 always() 下 PATCH 自有 id(FA3,01-ab-finalize-base-vs-head.png);空 id 时 head 零写入、base 会覆写其他 run 的终态评论(FA6);failed job 上 base 误报 "finished"、head 说 "ended early"(FA5);claim 两臂 body 字节一致,head 额外经真实 jq 导出 id(02-ab-claim-id-export.png);12 组合措辞真值表全对(03-edges-truth-table.png)。
  • 新增(本轮独有):main 已前进(95655daf),但两个被改文件在 main 上的 blob 与 base 完全一致;在 95655da 上实体化合并后重跑两套受影响测试,129/129 与 56/56 全绿 —— 合入当前 main 无冲突、无行为漂移。
  • 测试非空转:7 个点突变全部杀死(套件;其中 6 个同时被执行 harness 杀死),正向对照如期红,树逐字节复原(04-mutation-matrix.png)。
  • 门禁:vitest 129/129(head 与合并树各一次)、node --test 56/56(同)、actionlint 纯检查双树零发现 + shellcheck 增强发现头/base 集合恒等(均为存量、无一落在 PR hunks,05-gates.png)、四个抽取脚本 shellcheck/bash -n 全清(含 liveness 探针)、cache-key 引号翻转解析值恒等、改动测试文件 eslint 干净;yamllint 仍无法安装(pip3 Permission denied,已重证)。
  • 未覆盖:逐 commit 归因(depth-2 仅可达 1/25 commit);无真实已发评论校准 replay;GHA 服务端 timeout-minutesjob.status 语义;并发交错为推理+单元格证据,未在真实 API 上重放。

Previous-round finding status (re-measured at the same head)

Head and base are byte-identical to round 3 (no commits landed), so statuses cannot have moved — but each finding was re-measured anyway, not diffed from the old report.

# finding (round 3 at b8ac04ef) severity status at b8ac04ef (round 4)
1 Stranded "running" corner: an older own running marker behind a newer marker is not reclaimed (newest-wins by design); creation needs an export loss plus a newer marker informational stands, re-measured (cell EE5) — fixture [running 44 older, tombstone 45] → claim PATCHes newest 45, orphan 44 untouched, exports 45. Still strictly narrower than the round-1 shape and base-identical (base also selected newest-wins), so not a regression.
2 jq-null corner on the POST arm: a payload without .id exports literal comment_id=null, finalize PATCHes comments/null informational stands, re-measured (cells EE1a–c) — real jq emits literal null, [ -n ] accepts it, finalize PATCHes issues/comments/null; the best-effort guard still exits 0. Unreachable on the real comments API (201 always carries id).
3 Shared-slot interleavings transiently mislabel during overlapping runs; the qwen-triage-finalize.yml live-claim guard advertised by commit 4708d1ad never landed informational, pre-existing stands, re-verifiedgit diff HEAD^1..HEAD -- .github/workflows/qwen-triage-finalize.yml is empty; update_status still selects `

Declined rows: none in round 3.

Scope

Central claim: the lifecycle comment always reaches a terminal state because the two steps are coupled by id, not by re-selection: (a) the claim step exports the exact comment id it wrote (comment_id — reused id on PATCH success, --jq '.id'-extracted created id on POST, empty on any write failure); (b) Finalize triage status comment runs under always() && number != '' (covering cancellation, which success() || failure() excludes) and PATCHes exactly that id — never lists, never matches bodies, no-ops on an empty id; (c) the wording is correct for every terminal (TRIAGE_OUTCOME, JOB_STATUS) combination, and "finished" never appears on a failed job.

Secondary claims: (1) every write arm survives a failing API under GitHub's real -eo pipefail (exit 0 + warning), and a failed claim write exports an empty id rather than a guess; (2) the two cache-key key: quote-style flips (tmux-testing, verify jobs) are semantic no-ops.

Merge-staleness check (new this round)

The snapshot's baseRefOid (95655daf) is 8000+ enumerated commits ahead of the verified base tip a8fb97d16 (the count is shallow-graph-inflated; the ancestry relationship is unresolvable at depth 2). What matters is file-level:

  • Both files this PR touches are byte-identical on the new main tip to the verified base: blob 64e71788278fe8e17edd9953f4ad7c4de828f148 (qwen-triage.yml) and ea691f330c54ccde7c5a8730c62e65e7eb10450c (test file) at 95655daf == same blobs at a8fb97d16. A trial merge therefore applies the PR hunks verbatim with zero conflict possibility.
  • Materialized the merged tree (git worktree add tmp/merge-trial 95655daf, both files copied from head, hashes asserted equal to the head working-tree files), and re-ran the affected suites on the merged tree: vitest 129 passed (129), node --test 56 pass / 0 fail. Worktree removed afterwards (git worktree remove --force).
  • The only workflow-area churn on main since the base is qwen-autofix.yml and its test — a different lane, untouched by this PR.

A/B result — finalize step (central claim a–c)

Harness ab-finalize.mjs extracts each arm's if: + run: verbatim from the YAML, evaluates the if: under GitHub status-function semantics (success() and failure() are both false on cancellation — precisely the gap the PR closes; the evaluator refuses unknown expression shapes), and executes the bash under bash --noprofile --norc -eo pipefail with a recording gh stub + real jq. Fixture: this run's claim id 43, RUN_URL=…/runs/77. Witness: 01-ab-finalize-base-vs-head.png; raw log logs/ab-finalize.log.

cell base (list-and-flip-newest, success()‖failure()) head (PATCH exported id, always())
FA1 success/success, own 43 PATCH 43 "finished" PATCH 43 "finished", body byte-identical, exactly one API call
FA2 failure/failure, own 43 PATCH 43 "early" (half-width ZH comma) PATCH 43 "early" (full-width comma; bodies identical after normalizing that one character)
FA3 cancelled/cancelled (the bug) condition false → SKIPPED, 🔄 claim stranded forever PATCH 43 "was cancelled / 已取消", no other wording present
FA4 cancelled job, triage step success skipped PATCH 43 "finished" (triage success precedes the late cancel)
FA5 failed job, triage step success ('Check triage response' exits 1) PATCH 43 "finished" — points at stage comments that never posted PATCH 43 "ended early"
FA6 empty id; foreign tombstone 44 (runs/55) PATCHes 44 — clobbers the previous run's terminal wording zero writes + "claimed no status comment" diagnostic
FA7 empty id; empty thread POSTs a fresh "early" comment zero writes (accepted breadcrumb loss, documented in the step comment)
FA8 list API down, own 43 conflates unreadable with empty (‖ EXISTING_ID='') → POSTs a duplicate PATCHes 43 — makes no list call at all, immune
FA9 number='' skipped skipped (guard parity)
FA10 two pages, own on page 2, cancelled skipped PATCHes 43 with one call (no pagination idiom needed)

33/33 assertions, including the control cells asserting the base arm misbehaves exactly as predicted (FA3 skip, FA5 false "finished", FA6 clobber, FA8 conflation) — expected base failures count as passed assertions.

A/B result — claim step (central claim a, secondary 1)

ab-claim.mjs, same mechanics. The stub answers a POST with a JSON payload {"id": 7777} and applies the caller's --jq filter with real jq (as gh does client-side) — one fidelity level above the PR's own suite, whose stub echoes a bare id and therefore cannot exercise the --jq '.id' flag behaviorally (the suite pins it statically, and says so in a comment). Witness: 02-ab-claim-id-export.png; raw log logs/ab-claim.log.

cell base head
CA1 empty thread POST running claim POST byte-identical body; --jq .id in raw args; exports comment_id=7777 extracted by real jq
CA2 tombstones 42+44 PATCH newest 44 PATCH 44 (42 untouched), exports 44 (shared-slot semantics unchanged)
CA3 legacy marker 41 PATCH 41 PATCH 41, exports 41 (single-slot healing kept)
CA4 forged non-bot marker POST fresh (author filter) POST fresh, exports 7777
CA5 unreadable list POST fresh (‖ EXISTING_ID=''), exit 0 POST fresh, exit 0, exports 7777 (parity with base, plus the id)
CA6 POST-fail / PATCH-fail exit 0 + warning both arms exit 0 + warning, exports EMPTY id both arms (never a guessed/stale id)
CA7 bot identity failure exit 0, warn, no writes same; writes no comment_id= line at all (finalize sees empty)

29/29 assertions.

Head-only edge matrix + wording truth table

edges.mjs. Witness: 03-edges-truth-table.png; raw log logs/edges.log.

cell result
EE1a–c jq-null corner re-measured end-to-end: POST payload {} → claim exports comment_id=null (real jq) → finalize PATCHes issues/comments/null, still exit 0 (best-effort holds). Unreachable on the real comments API.
EE2 STATUS_COMMENT_ID fully unset (set -u safe via ${…:-}): exit 0, zero writes, diagnostic
EE3 census across all 12 combos: finalize makes exactly one call per run, all PATCH — no list, no identity call (the no-re-selection property)
EE4 exactly one comment_id= line per claim run in fresh / reuse / failed shapes
EE5 stranded-corner measurement (Finding 1): fixture [running 44 older, tombstone 45] → claim PATCHes newest 45, orphan 44 untouched, exports 45
EE6 finished/cancelled/early bodies each start with the lifecycle marker and keep [view run]($RUN_URL)
EE7 full truth table: 4 TRIAGE_OUTCOME × 3 JOB_STATUS = 12 combos, each asserting the expected wording EN+ZH present and the other two absent (the PR's own suite executes 7 of the 12); plus a dedicated sweep: no JOB_STATUS=failure combo ever says "finished"

27/27 assertions.

Mutation matrix (vacuity of the new tests)

Seven point mutants of the head YAML applied in place (each target asserted single-occurrence before mutating), the vitest suite re-run per mutant, plus the matching executed harness for the six mutants whose behavior a harness exercises; file restored byte-exactly after each (sha256-verified), controls re-run at the end. Unmutated controls: suite 129/129 green; all three harnesses green. Witness: 04-mutation-matrix.png; raw log logs/mutation-matrix.log.

mutant suite executed harness
M1 claim POST arm: drop --jq '.id' RED (1 test — the static pin) RED — ab-claim 25/29 (export becomes the raw payload, not 7777)
M2 swap cancelled↔early EN wording (positive control) RED (5 tests)
M3 drop the JOB_STATUS != failure gate on "finished" RED (2 tests) RED — edges 25/27 (truth table + failure sweep)
M4 remove the empty-id no-op guard RED (2 tests) RED — ab-finalize 30/33 (writes appear where none may)
M5 claim exports the reused id even when the PATCH fails RED (1 test) RED — ab-claim 28/29 (stale 45 exported)
M6 claim selector lastfirst RED (1 test) RED — ab-claim 26/29 (PATCHes 42, exports 42)
M7 finalize always()success()‖failure() RED (1 test) RED — ab-finalize 28/33 (condition shape + cancelled cell)

No survivors; no mutant regressed from killed to survived. M1 is the attribution the last commit claims: the executed harness catches the dropped flag behaviorally, where the suite pins it only statically (the suite's stub bypasses --jq). 32/32 assertions including the restore proofs and post-mutation controls; final git status clean.

Corrections

  • Round 3's correction still stands at this head (re-verified). Commit 4708d1ad describes teaching qwen-triage-finalize.yml's update_status to "prefer the newest marker that is not a live running claim"; the aggregate diff contains no change to that file (git diff HEAD^1..HEAD --stat -- .github/workflows/qwen-triage-finalize.yml empty), and the selector at head is still the unconditional | last | (line 328). This is a correction to the commit description, not a request to change the code: the harm that commit set out to fix no longer exists, because the finalize step now PATCHes the exported id — an update_status overwrite of a live claim's body can no longer break the flip. What remains is the pre-existing, base-identical cosmetic overwrite (Finding 3).

Findings (non-blocking, all carried from round 3 and re-measured)

  1. Stranded "running" comments can still persist in one narrow corner (informational). Re-measured in EE5: with [running 44 older, tombstone 45], the claim reuses newest 45 and never touches 44. Creation requires the claim's PATCH to commit server-side but lose its response (empty comment_id export while the comment carries this run's running body) AND a newer marker to appear later. Not a regression vs base (base also selected newest-wins and had no reclaim), and every run's own comment is finalized by id. No action required; optionally name the response-loss half in the step comment (it currently names only the write-failure half).
  2. jq-null corner on the POST arm (informational). Re-measured in EE1a–c: if a POST ever returned 200 without an id field, --jq '.id' emits literal null, [ -n ] accepts it, finalize PATCHes issues/comments/null, gh 404s, and the best-effort guard warns and exits 0. Unreachable on the real comments API. No action required.
  3. Shared-slot interleavings still transiently mislabel during overlapping runs; the advertised live-claim guard is absent (informational, pre-existing). The marker comment is a shared slot: run B's claim overwrites run A's running body, and A's later finalize flips the slot while B is live; the slot converges when B finalizes (EE7: all 12 wording combos land the correct terminal state). Base-identical and accepted by the slot design. Separately, see Corrections: update_status in qwen-triage-finalize.yml can still cosmetically overwrite a live claim until that run finalizes. No action required.

No new findings this round.

Not covered

  • Uncalibrated replay — no GitHub token and no real lifecycle comment available locally, so the harnesses were not byte-calibrated against a production-emitted artifact (same as rounds 1–3). Partial corroboration: the claim bodies the harnesses flip are the exact bytes the head claim step composes (CA1), and actionlint fully parses both trees' YAML.
  • Per-commit attribution — depth-2 checkout: git rev-list HEAD^1..HEAD^2 returns 1 commit (shallow boundary) against 25 in the metadata snapshot. The aggregate HEAD^1..HEAD diff (2 files) is what was verified.
  • yamllint gate — environmental, re-proven this round: pip3: Permission denied in scripts/lint.js --setup (quoted in logs/lint-setup.log). YAML validity still covered by actionlint's full parse on both trees and every harness's YAML parse.
  • GHA server-side semanticstimeout-minutesjob.status value, and job.status availability inside env: (round-2 finding carried through round 3, stands). actionlint validates the context usage; the wording branch handles both readings and the 12-combo truth table covers every pair.
  • Live concurrency replay against the real API — Finding 3's interleavings are reasoned + cell-measured, not replayed on GitHub.
  • Repo-wide eslint/lint — covered by the PR's own CI; the one changed non-workflow file passed a targeted npx eslint run (clean).
  • The PR description's test count ("120/120") is stale: the suite at head is 129 tests. No significance.

Methodology

Environment: the CI verify container (node:22-bookworm, node v22.23.2, jq 1.6; actionlint 1.7.12 + shellcheck installed pinned via scripts/lint.js --setup into /tmp/qwen-code-linters/local-*; yamllint uninstallable). The PR touches only workflow YAML and one test file — no compiled packages, no dependency changes — so the A/B "builds" are the two trees' YAML: head .github/workflows/qwen-triage.yml (sha256 8b7b4b94…) vs the base file dumped from git show HEAD^1 (sha256 909ba7fd…, equal to round-3's cited hash and to the blob at the new main tip 95655daf). The code under test is the verbatim-extracted bash executed under GitHub's exact shell flags with real jq and a recording gh stub that applies --jq filters like gh does and infers POST from -f fields like gh does (no stub of the code under test itself); if: conditions are evaluated under GitHub status-function semantics by a strict parser that refuses unknown shapes. Harnesses (in this dir): ab-finalize.mjs (33), ab-claim.mjs (29), edges.mjs (27), mutate.mjs (7 mutants + controls = 32), gates.mjs (23) = 144/144. Suite gates additionally executed (not counted in assertions.json): vitest 129/129 at head and on the merged tree; node --test 56/56 at head and on the merged tree. Mutation matrix: each mutant applied byte-exactly with single-occurrence asserted, suite (+ executed harness where applicable) re-run, file restored and sha256-verified; final git status clean. Raw per-run logs in logs/; evidence PNGs via scripts/verify-capture.mjs.

Evidence images

01-ab-finalize-base-vs-head

02-ab-claim-id-export

03-edges-truth-table

04-mutation-matrix

05-gates

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

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

The bot already has a review of its own on b8ac04ef99c37cab53b28d2c80cbfe55f853f557, which still stands.

机器人在 b8ac04ef99c37cab53b28d2c80cbfe55f853f557 上已有自己的评审,且仍然有效。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review round — no action needed (PR #8436)

Triaged all feedback newer than the last evaluation (2026-08-07T03:40:37Z). Nothing this round requires a code change:

  • Sandboxed verification report (@qwen-code-ci-bot): ✅ passed — merge-ready, 144/144 scripted assertions. It is advisory evidence, and its three carried findings (stranded-running corner, jq-null POST corner, shared-slot interleaving) are all explicitly informational, no action required — each is base-identical or unreachable on the real API. Its "Corrections" note is a correction to a commit description and explicitly "not a request to change the code".
  • Review by @yiliang114 (2026-08-07T03:52Z, confirmed in the raw review payload): APPROVED — "LGTM, no blockers… No P0/P1". No findings to address.
  • @wenshao's @qwen-code /triage comment is a triage-workflow trigger, not change feedback; the re-run completed with "no new review" and the bot's existing review still standing.
  • @qwen-code-dev-bot summary: "Reviewed the latest feedback — no changes needed" (its one open item, R8-9, targets the PR description, which this lane cannot edit and is author/maintainer action).
  • Failed checks: none. Still-red checks: none. Inline comments / reviews in the filtered feedback: none.

No code changes were made, no commits were added, and the branch remains at b8ac04ef9. No review threads were resolved or replied to (there are no open inline findings in this round's feedback).

Verification

No runnable verification commands were executed this round because no code was changed. Read-only checks actually run:

  • git status — clean working tree, branch fix/triage-status-comment-cancelled at b8ac04ef9
  • git diff origin/main...HEAD --stat — 2 files (.github/workflows/qwen-triage.yml, scripts/tests/qwen-triage-workflow.test.js), unchanged this round
  • Raw feedback payloads (ic.json, rc.json, rv.json) cross-checked against the filtered feedback.md — no actionable maintainer or automated-reviewer feedback was dropped by the filter (the only newer items are the approval, the /triage trigger, and bot status comments listed above)
中文说明

Autofix 评审轮次 — 无需操作(PR #8436

已对上次评估(2026-08-07T03:40:37Z)之后的全部反馈进行分类。本轮没有任何需要修改代码的事项:

  • 沙箱验证报告@qwen-code-ci-bot):✅ 通过 — 可合入(merge-ready),144/144 脚本断言全部通过。该报告仅为参考证据,其中沿袭的三条发现(滞留的 running 评论角落情形、POST 分支的 jq-null 角落情形、共享槽位交错)均明确标注为 informational(仅告知)、无需处理 —— 它们要么与 base 行为完全一致,要么在真实 API 上不可达。其 "Corrections" 一节是对某条 commit 描述的更正,并明确声明"不是要求修改代码"。
  • @yiliang114 的评审(2026-08-07T03:52Z,已在原始评审数据中核实):APPROVED — "LGTM, no blockers… No P0/P1"。没有任何需要处理的发现。
  • @wenshao@qwen-code /triage 评论是触发 triage 工作流的指令,不是修改意见;重跑已完成,结果为"无新评审",机器人既有的评审仍然有效。
  • @qwen-code-dev-bot 的总结:"已审阅最新反馈——无需改动"(其唯一未结事项 R8-9 针对的是 PR 描述,本 lane 无权编辑,属于作者/维护者操作)。
  • 失败的检查: 无。仍然标红的检查: 无。过滤后反馈中的行内评论/评审: 无。

本轮未修改任何代码、未新增任何提交,分支保持在 b8ac04ef9。未解决(resolve)或回复任何评审线程(本轮反馈中不存在未结的行内发现)。

验证

由于本轮未改动任何代码,未执行任何可运行的验证命令。实际执行的只读检查如下:

  • git status — 工作区干净,分支 fix/triage-status-comment-cancelled 位于 b8ac04ef9
  • git diff origin/main...HEAD --stat — 共 2 个文件(.github/workflows/qwen-triage.ymlscripts/tests/qwen-triage-workflow.test.js),本轮未发生任何变化
  • 已将原始反馈数据(ic.jsonrc.jsonrv.json)与过滤后的 feedback.md 交叉核对 —— 过滤器没有遗漏任何来自维护者或自动评审者的可操作反馈(更新的项目仅为上述批准评审、/triage 触发指令和机器人状态评论)

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


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

@wenshao

wenshao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ inconclusive — completed without a usable structured 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.

中文 — 判定:⚠️ 无法判定 · 已完成但无可用的结构化判定

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

No report.md was found in the run artifacts, so the report section is omitted — see the workflow run output.

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

Qwen Code · sandboxed verification

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at b8ac04ef99c37cab53b28d2c80cbfe55f853f557. The workflow change itself is clean — I found no code-level defect and would approve on the diff alone. I am posting a Comment instead of an approval because one Critical still stands, and it is the PR description, not the code.

The one blocker: the PR body still does not follow the template

I checked this against the current body rather than trusting the flag. It uses Summary / Changes / Verification; .github/pull_request_template.md requires What this PR does, Why it's needed, Reviewer Test Plan (with How to verify, Evidence (Before & After), Tested on), Risk & Scope, and Linked Issues. There is no Test Plan section at all. This is the same R8-9 that has survived ten rounds, and it survives them because the autofix path can push commits but cannot edit a PR body — so it needs one human edit. A paste-ready template-conformant body reflecting the current id-handoff design is already in the thread (issue comment 5211486662). Apply it and re-run @qwen-code /triage, and I will flip this to an approval; nothing in the code is holding it up.

Everything else checks out — all four round-6 Criticals are gone

  • R6 job-scope Critical: fixed. The finished branch is now gated on [ "${JOB_STATUS:-}" != 'failure' ], so a red job can no longer post ✅ and point at stage comments that were never written.
  • R6 harness-fidelity Critical: fixed. The executed harness now spawns bash --noprofile --norc -eo pipefail, matching what GitHub actually gives shell: bash. That matters here: a step's own set -uo pipefail does not turn -e back off, so a harness without -e would stay green while an unguarded write killed the real step.
  • The two ownership-selection Criticals no longer have a subject. The foreign / empty classification and the newest-marker-then-check-ownership selector are gone; finalize PATCHes exactly the id the claim step exported. That is the right shape — one selection in the lifecycle instead of two that can disagree.
  • The cross-workflow contract survives. Both bodies still start with <!-- qwen-triage lifecycle -->, so qwen-triage-finalize.yml's update_status — the third writer of this comment — still finds it.

What I verified myself

I extracted the two real step scripts from this commit's workflow and ran them under GitHub's exact shell flags against a stubbed gh, independently of the PR's own harness.

Finalize, all nine cases exit 0 (best-effort holds under -e), body always prefixed with the lifecycle marker, PATCH always aimed at the claimed id: green job → finished; triage ok + job cancelled → finished; triage ok + job failed → ended early; triage cancelled → cancelled; triage failed + job cancelled → cancelled; triage failed + job failed → ended early; triage skipped + job failed → ended early; empty id → no write at all, only the "claimed no status comment" note; PATCH failure → warns and still exits 0.

Claim, all six cases exit 0: no marker → POST, and the POST arm's --jq '.id' really does extract the id from a realistic comments-API payload (comment_id=90210) — that is the substance of the still-open R9-2, so the residual there is coverage, not correctness; own marker → PATCH reuse exporting the reused id; two own markers → newest wins (44, not 42); legacy marker → reused; foreign author → ignored, fresh POST; marker not at the start of the body → ignored, so the startswith gate holds.

The success + cancelledfinished combo initially looked like a mislabel to me, but the triage agent posts its stage comments during its own step, so a cancel arriving afterwards means the verdict already landed. It is a deliberate, tested choice, and I agree with it.

Nit, deferred

The two npm-ci-${{ hashFiles(...) }} cache-key requoting hunks are semantically identical YAML and unrelated to this fix. Past round five I would not widen the diff to remove them.

中文说明

b8ac04ef99 上完成审查。工作流改动本身是干净的——我没有发现任何代码层面的缺陷,单看 diff 我会批准。之所以提交 Comment 而不是批准,是因为仍有一条 Critical 成立,而它出在 PR 描述而非代码上。

唯一的阻断项:PR 正文仍未遵循模板。 我核对了当前正文而非仅凭标记:现在用的是 Summary / Changes / Verification,而 .github/pull_request_template.md 要求 What this PR doesWhy it's neededReviewer Test Plan(含 How to verifyEvidence (Before & After)Tested on)、Risk & ScopeLinked Issues,且完全没有 Test Plan 一节。这就是撑过十轮的那条 R8-9;它之所以一直存活,是因为 autofix 路径只能推提交、无法修改 PR 正文——所以需要人工改一次。线程里已经有一份反映当前 id-handoff 设计、可直接粘贴的模板化正文(issue comment 5211486662)。贴上后重新运行 @qwen-code /triage,我就把这条改为批准;代码侧没有任何东西在拖后腿。

其余全部通过——第 6 轮的四条 Critical 均已消除。 job 作用域那条已修复:finished 分支现在带 [ "${JOB_STATUS:-}" != 'failure' ] 门,红 job 不再可能发出 ✅ 并指向从未写出的阶段评论。测试保真度那条已修复:执行型 harness 现在以 bash --noprofile --norc -eo pipefail 启动,与 GitHub 实际给 shell: bash 的一致——这在此处很关键,因为步骤自身的 set -uo pipefail 并不会关掉 -e,缺少 -e 的 harness 会在真实步骤已被未加保护的写操作打挂时仍然全绿。另外两条关于归属选择的 Critical 已无对象:foreign / empty 分类与「先取最新 marker 再判归属」的选择器都已移除,finalize 直接 PATCH 认领步骤导出的那个 id——这个形态是对的,生命周期里只剩一次选择,而不是两次可能互相矛盾的选择。跨工作流契约仍然成立:两处正文都以 <!-- qwen-triage lifecycle --> 开头,qwen-triage-finalize.ymlupdate_status(该评论的第三个写入方)仍能找到它。

我自己的验证: 我从本提交的工作流中抽出两个真实步骤脚本,在 GitHub 完全相同的 shell 参数下针对 stub 过的 gh 执行,独立于本 PR 自带的 harness。finalize 九种情形全部 exit 0-e 下 best-effort 成立),正文始终以生命周期 marker 开头,PATCH 始终指向认领的 id:绿 job → finished;triage 成功 + job 取消 → finished;triage 成功 + job 失败 → ended early;triage 取消 → cancelled;triage 失败 + job 取消 → cancelled;triage 失败 + job 失败 → ended early;triage 跳过 + job 失败 → ended early;空 id → 完全不写,只输出「未认领评论」提示;PATCH 失败 → 告警并仍然 exit 0。claim 六种情形全部 exit 0:无 marker → POST,且 POST 分支的 --jq '.id' 在真实形状的 comments API 载荷上确实提取出了 id(comment_id=90210)——这正是仍未关闭的 R9-2 的实质,因此那里的遗留是覆盖问题而非正确性问题;自有 marker → PATCH 复用并导出复用的 id;两条自有 marker → 取最新(44 而非 42);legacy marker → 复用;他人作者 → 忽略并新发 POST;marker 不在正文开头 → 忽略,说明 startswith 门有效。success + cancelledfinished 这一组我起初以为是误标,但 triage agent 的阶段评论是在它自己那一步里发出的,因此之后到达的取消意味着结论已经落地——这是有意为之且有测试钉住的选择,我同意。

已推迟的小问题: 两处 npm-ci-${{ hashFiles(...) }} 缓存 key 的引号改写在 YAML 语义上完全等价,且与本次修复无关。已过第 5 轮,我不会为了删掉它们而扩大 diff。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

The bot already has a review of its own on b8ac04ef99c37cab53b28d2c80cbfe55f853f557, which still stands.

机器人在 b8ac04ef99c37cab53b28d2c80cbfe55f853f557 上已有自己的评审,且仍然有效。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving at b8ac04ef99c37cab53b28d2c80cbfe55f853f557 — same commit I reviewed above; head, body, and check results are unchanged since then, so that verification still stands.

Why this is an approval

The workflow change is correct and the code side is clean. All four round-6 Criticals are gone at this commit:

  • Job-scope Critical: fixed. The finished branch is gated on [ "${JOB_STATUS:-}" != 'failure' ], so a red job can no longer post ✅ and point at stage comments that were never written.
  • Harness-fidelity Critical: fixed. The executed harness spawns bash --noprofile --norc -eo pipefail, matching what GitHub actually gives shell: bash — which matters precisely because a step's own set -uo pipefail does not turn -e back off.
  • The two ownership-selection Criticals no longer have a subject. The foreign / empty classification and the newest-marker-then-check-ownership selector are gone; the claim step exports the id it wrote and finalize PATCHes exactly that id. One selection in the lifecycle instead of two that can disagree — that is the right shape, and it is what makes the original fix (always() covering cancellation) safe rather than racy.
  • Cross-workflow contract preserved. Both bodies still start with <!-- qwen-triage lifecycle -->, so qwen-triage-finalize.yml's update_status — the third writer of this comment — still finds it.

I verified the behaviour myself rather than reading the assertions: I extracted the two real step scripts from this commit and ran them under GitHub's exact shell flags against a stubbed gh. Finalize, nine cases, all exit 0, body always marker-prefixed, PATCH always aimed at the claimed id — green→finished, triage-ok+cancelled→finished, triage-ok+failed→ended early, triage-cancelled→cancelled, triage-failed+cancelled→cancelled, triage-failed+failed→ended early, triage-skipped+failed→ended early, empty id→no write at all, PATCH failure→warns and still exits 0. Claim, six cases, all exit 0 — no marker→POST with the real --jq '.id' extracting the id from a realistic payload, own marker→PATCH reuse, two own markers→newest wins, legacy marker→reused, foreign author→ignored, marker not at body start→ignored.

Deferred, recorded so it is not silently dropped

R8-9 (PR description does not follow .github/pull_request_template.md) is not fixed — it is being deferred by maintainer decision, superseding my earlier Comment on this PR. The body still uses Summary / Changes / Verification with no Reviewer Test Plan, Risk & Scope, or Linked Issues section. It is a description-formatting item with no bearing on the code, it has survived ten rounds only because the autofix path can push commits but cannot edit a PR body, and per the review-round guidance this PR should now land Critical code fixes only. A paste-ready template-conformant body reflecting the current id-handoff design is in the thread (issue comment 5211486662) if anyone wants to apply it before merge.

Also deferred: the R9-2 coverage gap (the PR's own stub answers a POST with the already-extracted id, so only the static --jq '.id' pin guards that flag — I confirmed the extraction itself works, so this is coverage, not correctness), the R9-3 accepted loss of the pre-PR list-then-POST recovery path when the claim's write fails (documented in the step comment), and the two unrelated npm-ci-${{ hashFiles(...) }} cache-key requoting hunks, which are semantically identical YAML.

中文说明

b8ac04ef99c37cab53b28d2c80cbfe55f853f557 上批准 —— 与我上一条评审是同一个提交,head、正文与检查结果自那时起均未变动,因此那次验证依然有效。

为什么给批准: 工作流改动是正确的,代码侧是干净的,第 6 轮的四条 Critical 在本提交上全部消除。job 作用域那条已修复:finished 分支带 [ "${JOB_STATUS:-}" != 'failure' ] 门,红 job 不再可能发出 ✅ 并指向从未写出的阶段评论。测试保真度那条已修复:执行型 harness 以 bash --noprofile --norc -eo pipefail 启动,与 GitHub 实际给 shell: bash 的一致——这恰恰关键,因为步骤自身的 set -uo pipefail 并不会关掉 -e。另外两条关于归属选择的 Critical 已无对象:foreign / empty 分类与「先取最新 marker 再判归属」的选择器都已移除,认领步骤导出它写入的 id,finalize 精确 PATCH 该 id——生命周期里只剩一次选择而非两次可能互相矛盾的选择,这个形态是对的,也正是它让最初那个修复(用 always() 覆盖取消)从「有竞态」变成「安全」。跨工作流契约保持不变:两处正文仍以 <!-- qwen-triage lifecycle --> 开头,qwen-triage-finalize.ymlupdate_status(该评论的第三个写入方)仍能找到它。

我没有只读断言,而是自己验证了行为:从本提交抽出两个真实步骤脚本,在 GitHub 完全相同的 shell 参数下针对 stub 过的 gh 执行。finalize 九种情形全部 exit 0,正文始终以 marker 开头,PATCH 始终指向认领的 id——绿 job→finished、triage 成功+取消→finished、triage 成功+失败→ended early、triage 取消→cancelled、triage 失败+取消→cancelled、triage 失败+失败→ended early、triage 跳过+失败→ended early、空 id→完全不写、PATCH 失败→告警并仍然 exit 0。claim 六种情形全部 exit 0——无 marker→POST 且真实的 --jq '.id' 从真实形状载荷中取出了 id、自有 marker→PATCH 复用、两条自有 marker→取最新、legacy marker→复用、他人作者→忽略、marker 不在正文开头→忽略。

已推迟并在此记录,避免被静默丢弃:R8-9(PR 描述未遵循 .github/pull_request_template.md)并未修复,而是按维护者决定推迟处理,本条取代我此前在本 PR 上的 Comment 立场。 正文仍使用 Summary / Changes / Verification,缺少 Reviewer Test PlanRisk & ScopeLinked Issues 各节。这是描述格式问题,与代码无关;它之所以撑过十轮,仅因为 autofix 路径只能推提交、无法修改 PR 正文;按评审轮次约定,本 PR 此时应只落 Critical 级别的代码修复。若希望在合并前补上,线程里(issue comment 5196984782 的更新版 5211486662)已有一份反映当前 id-handoff 设计、可直接粘贴的合规正文。

同时推迟的还有:R9-2 的覆盖缺口(本 PR 自带的 stub 对 POST 直接返回已提取的 id,因此该标志只由静态的 --jq '.id' 断言把关——我已确认提取本身可用,所以这是覆盖问题而非正确性问题)、R9-3 中被接受的「认领写入失败时丢失 PR 前的 list-then-POST 恢复路径」(已在步骤注释中记录),以及两处与本次修复无关的 npm-ci-${{ hashFiles(...) }} 缓存 key 引号改写(YAML 语义完全等价)。

@wenshao
wenshao enabled auto-merge August 7, 2026 06:28
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review round: no code changes — the one remaining blocker needs a single manual PR-body edit

The new feedback on b8ac04ef99 was triaged. The reviewer independently re-verified both workflow step scripts under GitHub's exact shell flags and found no code-level defect; all four round-6 Criticals are confirmed gone. Nothing in the code is holding this PR up.

Feedback triage

  1. [Critical] PR body does not follow the template (R8-9)escalated: needs a maintainer's manual edit; this flow cannot perform it. The body currently uses Summary / Changes / Verification, while .github/pull_request_template.md requires What this PR does, Why it's needed, Reviewer Test Plan (with How to verify, Evidence (Before & After), Tested on), Risk & Scope, and Linked Issues. Editing the PR body is a GitHub write this autofix path cannot make: the address-review flow can only push commits, and qwen-autofix.yml has no body-update step anywhere (its gh pr edit calls only touch labels; pr-body.md is consumed solely at PR creation in the develop-issue flow). Action for a maintainer: apply the paste-ready template-conformant body already in the thread (issue comment 5211486662) — via the GitHub UI or gh pr edit 8436 --body-file <file> — then re-run @qwen-code /triage. The reviewer committed to flipping to an approval once the body conforms.
  2. [Nit] npm-ci-${{ hashFiles(...) }} cache-key requoting hunksdeferred, no action. Semantically identical YAML unrelated to the fix; the reviewer explicitly declined to widen the diff to remove them past round five. The deferral is respected and the hunks stay as committed.
  3. Sandboxed-verification bot comment: inconclusiveinformational only. Advisory note that the verify run completed without a structured verdict or report artifact; it is not code feedback on this PR, so there is nothing to address here.

No code changes were made and nothing was committed this round; the tree is unchanged at b8ac04ef99. No verification commands were run because nothing changed — this exact commit already passed the deterministic gate at push time.

中文说明

Autofix 审查轮次:无代码改动 —— 唯一剩余的阻断项需要一次性手工修改 PR 正文

b8ac04ef99 上的新反馈完成了分类。审查者已在 GitHub 完全相同的 shell 参数下独立复核了两个工作流步骤脚本,未发现任何代码层面的缺陷;第 6 轮的四条 Critical 均已确认消除。代码侧没有任何阻碍本 PR 合入的问题。

反馈分类

  1. [Critical] PR 正文未遵循模板(R8-9) —— 升级处理:需要维护者手工修改一次,本流程无法代办。 正文目前使用 Summary / Changes / Verification,而 .github/pull_request_template.md 要求 What this PR doesWhy it's neededReviewer Test Plan(含 How to verifyEvidence (Before & After)Tested on)、Risk & ScopeLinked Issues。修改 PR 正文属于 GitHub 写操作,本 autofix 路径无法执行:address-review 流程只能推送提交,qwen-autofix.yml 中任何位置都没有更新正文的步骤(其中的 gh pr edit 调用只操作标签;pr-body.md 仅在 develop-issue 流程创建新 PR 时被消费)。需要维护者执行的操作: 将线程中已备好的、符合模板且可直接粘贴的正文(issue comment 5211486662)应用上 —— 通过 GitHub 界面或 gh pr edit 8436 --body-file <file> —— 然后重新运行 @qwen-code /triage。审查者已承诺正文符合模板后即改为批准。
  2. [Nit] npm-ci-${{ hashFiles(...) }} 缓存 key 引号改写 —— 按推迟处理,不改动。 语义完全等价的 YAML,与本修复无关;审查者明确表示在第 5 轮之后不愿为移除它们而扩大 diff。尊重该推迟决定,这两处保持已提交状态不变。
  3. 沙箱验证机器人评论:无法判定 —— 仅记录。 这是验证运行完成但无结构化判定、无报告产物的提示信息,并非针对本 PR 代码的反馈,此处无可处理项。

本轮未做任何代码改动,也未提交任何内容;工作树保持在 b8ac04ef99 不变。由于没有改动,本轮未运行验证命令 —— 该提交在推送时已通过确定性门禁。

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


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

@wenshao
wenshao added this pull request to the merge queue Aug 7, 2026
Merged via the queue into QwenLM:main with commit 18b9251 Aug 7, 2026
113 of 122 checks passed
pixel-bits pushed a commit to pixel-bits/qwen-code that referenced this pull request Aug 7, 2026
…ne (QwenLM#8676)

* feat(autofix): bridge fork-PR reviews into the credentialed review lane

A workflow run tied to a pull request whose head lives in a fork is
created with `Secret source: None`, so `secrets.CI_DEV_BOT_PAT` is empty
and the autofix review lane cannot authenticate. Those reviews therefore
wait for the scheduled scan, which GitHub throttles hard — the `*/10`
cron lands every 40-70 minutes on this repo.

Reading the PR with `github.token` instead would not help: review-address
still needs the PAT to push and comment, and it is empty for the whole
run. The credential has to come from a different run.

This is the standard `workflow_run` bridge for that.

- `qwen-autofix-fork-signal.yml` runs on `pull_request_review`, where the
  fork PR's event can reach it, and records only which PR was reviewed.
  It is deliberately powerless: `permissions: {}`, no checkout, no
  repository code, and the hosted pool rather than the persistent
  self-hosted one.
- `qwen-autofix-fork-bridge.yml` runs on `workflow_run`, which GitHub
  creates on the default branch with the repository's own token, and
  dispatches `qwen-autofix.yml` for that PR using `actions: write` — the
  same lever Fleet Shepherd already pulls. It holds no PAT.

The dispatched run re-derives admission from live API state, so the PR
number says when to look, never who may be touched. It is bound to
`workflow_run.head_sha` regardless: the signal run carries the reviewed
PR's head SHA, and only that PR has that head.

Measured, not assumed:

- `workflow_run.pull_requests` is empty for fork PRs, and
  `/commits/{sha}/pulls` does not resolve a fork head either (it does
  resolve an in-repo one), so the artifact is the only way to learn the
  number.
- `pull_request_review` runs the workflow file from the BASE branch: run
  31152873061's PR branch predates a main-only change to
  `qwen-autofix.yml` and it still executed the new file. A fork cannot
  edit what the signal writes.
- That run's `head_sha` equals PR QwenLM#8436's `headRefOid` exactly, which is
  what makes the binding possible.

The bridge's validate-and-dispatch block is replayed under bash against a
stub API: forgery, an unreadable head, both heads empty, non-numeric and
path-shaped artifacts, a closed or retargeted PR, download and read
failures, and dispatch retry then exhaustion.

* fix(autofix): align fork bridge with route admission and coalesce bridged dispatches

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

* fix(autofix): harden the fork-review bridge per review feedback (QwenLM#8676)

---------

Co-authored-by: verify <verify@local>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.8.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants