Skip to content

feat(review): remember explicit effort per project - #9883

Merged
wenshao merged 6 commits into
QwenLM:mainfrom
tlysanhuo:tlysanhuo/review-last-effort
Aug 27, 2026
Merged

feat(review): remember explicit effort per project#9883
wenshao merged 6 commits into
QwenLM:mainfrom
tlysanhuo:tlysanhuo/review-last-effort

Conversation

@tlysanhuo

@tlysanhuo tlysanhuo commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Persists the last /review --effort level the user explicitly typed in user-private, project-scoped storage. A later /review without --effort reuses that value, reports effortSource: "last_used", and emits a notice that the review skill relays before work begins.

The resolution order is explicit --effort > remembered level > operator review.effort > target default. Effective --comment and --fix safety requirements still override the resolved level. An explicit value is remembered even when it equals the target default. The value is written atomically with mode 0600, using the parent session's QWEN_CODE_PROJECT_DIR as the project owner when available.

Remembered-state I/O is fail-open and visible: malformed or unreadable state is reported on stderr and ignored so resolution continues from review.effort and the target default; a persistence failure is reported without discarding the explicit effort selected for the current review.

Resume handling also passes remembered and configured levels to fetch-pr, so a mismatch starts fresh at the level required by the current invocation instead of silently pinning the interrupted run's level.

Why it's needed

Users who repeatedly choose the same review depth currently have to type the same flag on every invocation. The existing review.effort setting is an operator-wide default, while an explicitly selected review depth is project-specific user intent. Remembering only explicitly typed levels preserves that intent without turning automatic or target-default decisions into persistent policy.

Reviewer Test Plan

How to verify

  1. Build Core and CLI, then run printf '%s\n' '--effort high 9792' | QWEN_CODE_PROJECT_DIR="$PROJECT_DIR" node packages/cli/dist/index.js review parse-args --stdin. Confirm the verdict reports "effort": "high" and "effortSource": "explicit".
  2. Run printf '%s\n' '9792' | QWEN_CODE_PROJECT_DIR="$PROJECT_DIR" node packages/cli/dist/index.js review parse-args --stdin from the same project. Confirm it reports "effort": "high", "effortSource": "last_used", and the reuse notice.
  3. Repeat with an explicit --effort medium; confirm the next invocation reuses medium. Also confirm an explicit value overwrites malformed remembered state, malformed state without an explicit replacement falls through with a NOTE:, and a simulated persistence failure does not block the explicitly requested effort.
  4. Run the focused tests and typechecks below.
npm run typecheck --workspace @qwen-code/qwen-code
npm run typecheck --workspace @qwen-code/qwen-code-core
cd packages/cli && npx vitest run src/commands/review/parse-args.test.ts src/commands/review/lib/paths.test.ts src/config/settingsSchema.test.ts
cd packages/core && npx vitest run src/skills/bundled/review/SKILL.test.ts

Evidence (Before & After)

Before: an explicitly typed effort applied only to the current invocation; the next invocation without --effort resolved from review.effort or the target default.

After: the first invocation reports {"effort":"high","effortSource":"explicit"} and persists high; the next invocation reports {"effort":"high","effortSource":"last_used"} plus No effort level given — reusing high, the level you typed last time. Focused tests pass on the rebased head: CLI 214/214, Core skill 42/42.

Tested on

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

Environment (optional)

macOS 15.3.1, Node.js 24.13.1, npm 11.8.0; built CLI exercised end to end with an isolated project owner directory.

Risk & Scope

  • Main risk or tradeoff: a remembered explicit choice overrides review.effort for that project until another explicit choice replaces it.
  • Not validated / out of scope: manual Windows and Linux runs; a separate UI for inspecting or clearing the remembered value.
  • Breaking changes / migration notes: none. Projects without a remembered value retain the current configured/default behavior.

Linked Issues

Fixes #9792

中文说明

本 PR 做了什么

将用户最近一次显式输入的 /review --effort 档位持久化到用户私有、按项目隔离的存储中。之后调用 /review 时如果没有提供 --effort,参数解析会复用该值,返回 effortSource: "last_used",并生成一条提示,由 review skill 在开始工作前转达给用户。

解析优先级为:显式 --effort > 项目记忆值 > 操作者配置 review.effort > 目标类型默认值。生效的 --comment--fix 安全要求仍会覆盖最终档位。即使显式输入恰好等于目标默认值,也会被记住。该值使用原子写入并设置为 0600 权限;父 session 提供 QWEN_CODE_PROJECT_DIR 时,沿用该目录作为项目存储归属。

记忆状态的 I/O 采用可见的 fail-open 语义:损坏或无法读取的状态会在 stderr 报告后被忽略,并继续按 review.effort 与目标默认值解析;持久化失败会被报告,但不会丢弃本次 review 已显式选定的档位。

恢复中断 review 时,也会把记忆值和配置值传给 fetch-pr。如果与中断任务记录的档位不一致,就按当前调用要求的档位重新开始,不会静默固定在旧档位。

为什么需要

用户如果经常为同一项目选择相同的 review 深度,现在每次都必须重复输入同一个参数。现有 review.effort 是操作者范围的通用默认值,而用户显式选择的 review 深度属于项目级意图。仅记忆显式输入可以保留这个意图,同时不会把自动选择或目标默认值悄悄固化成长期策略。

Reviewer 测试计划

如何验证

  1. 构建 Core 和 CLI,然后执行 printf '%s\n' '--effort high 9792' | QWEN_CODE_PROJECT_DIR="$PROJECT_DIR" node packages/cli/dist/index.js review parse-args --stdin。确认结果包含 "effort": "high""effortSource": "explicit"
  2. 在同一项目中执行 printf '%s\n' '9792' | QWEN_CODE_PROJECT_DIR="$PROJECT_DIR" node packages/cli/dist/index.js review parse-args --stdin。确认结果包含 "effort": "high""effortSource": "last_used" 和复用提示。
  3. 再显式输入 --effort medium,确认下一次调用会复用 medium;同时确认显式输入可以覆盖损坏的旧记忆状态、无显式值时损坏状态会伴随 NOTE: 回落,以及模拟的持久化失败不会阻断显式请求的档位。
  4. 运行上面的定向测试和类型检查命令。

证据(修改前与修改后)

修改前:显式输入的 effort 仅对当前调用生效;下一次省略 --effort 时,会重新使用 review.effort 或目标类型默认值。

修改后:第一次调用返回 {"effort":"high","effortSource":"explicit"} 并持久化 high;下一次调用返回 {"effort":"high","effortSource":"last_used"},同时提示 No effort level given — reusing high, the level you typed last time. rebase 后的定向测试全部通过:CLI 214/214,Core skill 42/42

测试平台

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

环境(可选)

macOS 15.3.1、Node.js 24.13.1、npm 11.8.0;使用隔离的项目归属目录对构建后的 CLI 做了端到端验证。

风险与范围

  • 主要权衡:项目记忆的显式选择会覆盖该项目的 review.effort,直到用户再次显式输入其他档位。
  • 未验证或不在范围内:Windows 和 Linux 手工运行;单独用于查看或清除记忆值的 UI。
  • 破坏性变更或迁移说明:无。没有记忆值的项目继续保持现有配置和默认行为。

关联 Issue

Fixes #9792

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Re-run at the current head, after three rounds of review and the maintainer's re-verification.

Template: complete and filled in now ✓ — what/why, a step-by-step reviewer test plan with expected verdict JSON, before/after evidence, risk & scope, and the bilingual summary. The two gaps from the last pass (untouched template, fail-open question) are both closed: the body documents the fail-open I/O semantics, and the code implements them.

Problem: real and observed — filed as #9792 by a maintainer with a concrete proposal: /review resolves effort from the target type on every invocation, so a user who always wants a given level retypes --effort every time. The reference agent (Claude Code) ships exactly this behavior, and the issue spells out the intended precedence and notice.

Direction: aligned. /review mirrors the reference agent's review surface, and remembering the last explicitly typed level — explicit only, never a defaulted one — is the same scoping choice the issue proposes. CHANGELOG reference as before: "Changed /code-review with no effort level to reuse the level you typed last".

Size: small and test-heavy — 187 production lines (149 in parse-args.ts, 21 in paths.ts, 2 in settingsSchema.ts, 15 in the bundled review SKILL.md) vs 331 test lines, plus 8 doc lines and 2 lines of generated VS Code schema. Well under every threshold; no maintainer escalation on size.

Approach: scope feels right, and it stayed minimal across the three rounds. The remembered value can't live in settings (Workspace scope is deliberately ignored for review.effort, user settings are global), so a user-private file keyed on Storage's project dir is the minimal mechanism; the diff is parser wiring, a path helper, fail-open I/O, docs in all three places, and tests. No drive-by changes. The remembered-beats-configured precedence is documented as intent in the PR body and matches the reference agent.

Risk: no high-risk path matches; no elevated risk signals.

Moving on to code review. 🔍

中文说明

在当前 head 上重跑——此前已经过三轮审查和 maintainer 的复验。

模板:现已完整填写 ✓ ——做了什么/为什么、带预期 verdict JSON 的逐步验证计划、before/after 证据、风险与范围、中文摘要。上一轮的两个缺口(未填写的模板、fail-open 疑问)都已闭合:正文写明了 fail-open I/O 语义,代码也已实现。

问题:真实且已被观测——由 maintainer 以 #9792 提出并给出具体方案:/review 每次按目标类型回落默认档,习惯某一档位的用户每次都要重打 --effort。参考实现(Claude Code)已上线同样行为,issue 也写明了预期的优先级与提示语。

方向:对齐。/review 与参考实现的 review 面对齐,仅记住显式输入的档位(默认值不覆写记忆)也正是 issue 提出的取舍。

规模:小而测试厚重——生产代码 187 行(parse-args.ts 149、paths.ts 21、settingsSchema.ts 2、bundled review SKILL.md 15),测试 331 行,另有 8 行文档与 2 行生成的 VS Code schema。远低于所有阈值,无需因规模升级。

方案:范围合理,且三轮之后仍保持最小。记忆值不能放在 settings 里(Workspace 作用域被刻意忽略、用户设置是全局的),所以按 Storage 项目目录键控的用户私有文件是最小机制;diff 只含解析器接线、路径助手、fail-open I/O、三处文档与测试。没有顺手改动。记忆值优先于配置值在正文中作为意图写明,且与参考实现一致。

风险:未命中高风险路径;无升级风险信号。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Independent baseline: for "remember the last explicitly typed effort per project" I'd persist an explicit choice in user-private, project-keyed state (settings are out — Workspace scope is deliberately ignored for review.effort), resolve explicit > remembered > configured > target default with the --comment/--fix forcings still on top, announce the reuse, never persist a defaulted level, and fail-open on I/O because this file sits on the review's critical path. The PR matches that shape exactly, and the three review rounds hardened exactly the right places.

What I verified against the base code and the current diff:

  • Consumers named. All four existing parseReviewArgs call sites (run.ts, authorization.ts ×3) call it with one or two arguments; the new third parameter (rememberExplicitEffort) is optional, so they are signature-compatible and behavior-unchanged. The only memory wiring point is the parse-args command handler via parseReviewArgsWithMemory; the only consumer of the new last_used source is the bundled review skill, updated in the same diff and pinned by SKILL.test.ts.
  • The double-parse is safe. parseReviewArgsWithMemory runs the parser once with a capture callback, and only re-parses when no explicit effort was typed; parseReviewArgs accumulates warnings in a local array and performs no I/O, so the second pass cannot duplicate a side effect. The write callback fires only for a valid explicit effort — --effort bogus neither records nor clobbers memory (pinned by test).
  • Fail-open on both sides — the round-1 blocker is closed. readLastReviewEffort catches read faults and reports a NOTE: on stderr, and treats invalid content the same way (warn, ignore, resolve from review.effort and the target default); writeLastReviewEffort catches mkdirSync/atomicWriteFileSync faults and reports without discarding the resolved effort — naming the resolved level, not the typed one, so a forced run's NOTE is honest (pinned: --comment --effort low + ENOSPC → "this review still uses high"). This matches the adjacent operatorReviewSettings() degrade policy that the round-1 review cited.
  • Storage hygiene. atomicWriteFileSync with { mode: 0o600, forceMode: true, noFollow: true } and a 0o700 parent — the same option set oauth-token-storage.ts already uses for user-private state; the path prefers the parent session's QWEN_CODE_PROJECT_DIR and falls back to new Storage(projectRoot).getProjectDir() (verified that API exists). Never inside the repo.
  • Ordering. The last_used arm sits between explicit and configured; the comment/fix forcings run after resolution, so a forced run never claims a remembered level (pinned: --comment + remembered low → forced-by-comment).
  • Resume semantics. SKILL.md now enumerates pass-through as explicit / last_used / configured / forced-by-comment, omit only default — resolving a genuine ambiguity in the old wording (which left configured undecided, toward passing). That is a disclosed behavior change, documented in both docs files; the maintainer's re-verification calls it the right call.

No blockers. The maintainer's remaining observations from re-verification are all explicitly deferred follow-ups, and I concur they don't hold this merge: (a) the read path is fail-open for errors but a FIFO at the memory path would block readFileSync, and the got ${JSON.stringify(value)} NOTE echoes the whole unusable file — his measured one-line fix (statSync(...).isFile() + a .slice(0, 40)) is worth a follow-up issue; it needs same-user write access to a 0700 directory, so it's hardening, not a privilege boundary; (b) there is still no supported reset path (deleting the file by hand), scoped out by the PR and the issue; (c) the configured resume pass-through, disclosed above.

Test evidence

Unattended re-run — the evidence below is the PR's own CI on the reviewed commit, fetched via the API; nothing was built or executed locally. The unit suite pins the central claim end-to-end: the wiring test drives parseArgsCommand.handler twice — first --effort high (asserting the exact 0o600/noFollow write), then flag-less (asserting effort: high, effortSource: last_used, and the reuse warning) — plus precedence, forcings, invalid values, malformed-without-replacement, unreadable-state, write-failure, and resume-shape cases. On top of the suite, the maintainer drove the built binary A/B against the merge-base twice (round 1 on Linux, re-verification of this exact head on macOS) — fault injection, symlink plants, 24-way concurrency, and mutation tests confirming the new fail-open tests actually discriminate — and approved at this SHA. Both pull_request workflow runs are green; macOS/Windows/integration legs are the repo's merge_group-only surface (skipped by design).

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

Check Conclusion
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

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

The behavioral claim is substantiated by 2b (the suite pins the flow through the real handler) plus the maintainer's two built-binary A/B rounds, so no sandboxed-lane line is needed this time.

中文说明

代码审查

独立方案:对"按项目记住上次显式输入的 effort",我会把显式选择持久化到用户私有、按项目键控的状态里(settings 不可行——review.effort 刻意忽略 Workspace 作用域),按 显式 > 记忆 > 配置 > 目标默认 解析且 --comment/--fix 强制仍居其上,复用时告知,默认值从不持久化,且 I/O 必须 fail-open——这个文件位于 review 的关键路径上。PR 与该形状完全一致,三轮审查强化的也正是该强化的地方。

对照基线代码与当前 diff 验证过的点:

  • 消费者已点名。 现有四处 parseReviewArgs 调用(run.tsauthorization.ts ×3)都只传一到两个参数;新增第三参(rememberExplicitEffort)可选,签名兼容、行为不变。唯一的记忆接线点是 parseArgsCommand handler 经 parseReviewArgsWithMemory;新增 last_used 的唯一消费者是 bundled review skill,同一 diff 中更新并由 SKILL.test.ts 钉住。
  • 双解析是安全的。 parseReviewArgsWithMemory 先带捕获回调解析一次,仅在未出现显式 effort 时才二次解析;parseReviewArgs 把警告累积在局部数组且不做任何 I/O,二次解析不会复制副作用。写回调只对合法的显式值触发——--effort bogus 既不记录也不覆盖记忆(有测试钉住)。
  • 两侧均 fail-open——第一轮的阻断项已闭合。 readLastReviewEffort 捕获读取故障并在 stderr 打 NOTE:,非法内容同样处理(警告、忽略、改从 review.effort 与目标默认解析);writeLastReviewEffort 捕获 mkdirSync/atomicWriteFileSync 故障并报告、不丢弃已解析档位——且 NOTE 里写的是解析后的档位而非输入的,强制运行的 NOTE 因此诚实(已钉住:--comment --effort low + ENOSPC → "this review still uses high")。这与第一轮引用的相邻 operatorReviewSettings() 降级策略一致。
  • 存储卫生。 atomicWriteFileSync 使用 { mode: 0o600, forceMode: true, noFollow: true },父目录 0o700——与 oauth-token-storage.ts 对用户私有状态使用的选项集完全相同;路径优先父 session 的 QWEN_CODE_PROJECT_DIR,回退到 new Storage(projectRoot).getProjectDir()(已核实该 API 存在)。从不在仓库内。
  • 顺序。 last_used 分支位于显式与配置之间;comment/fix 强制在解析之后生效,强制运行不会声称记忆档位(已钉住:--comment + 记忆 low → forced-by-comment)。
  • Resume 语义。 SKILL.md 现把透传枚举为 explicit / last_used / configured / forced-by-comment,仅 default 省略——消解了旧措辞中真实存在的歧义(configured 悬而未决,现为透传)。这是有披露的行为变更,两处文档均已写明;maintainer 复验认为取舍正确。

无阻断项。maintainer 复验中剩余的观察均为其明确延后的后续项,我也认为不应阻塞本次合并:(a) 读路径对错误 fail-open,但记忆路径上放一个 FIFO 会阻塞 readFileSync,且 got ${JSON.stringify(value)} 的 NOTE 会整文件回显——他已实测的一行修复(statSync(...).isFile() + .slice(0, 40))值得开个后续 issue;该攻击面需要同用户写入 0700 目录的权限,属加固而非权限边界;(b) 仍无受支持的清除路径(手工删文件),PR 与 issue 均已明确排除在范围外;(c) configured 的 resume 透传,如上已披露。

测试证据

无人值守重跑——以下证据来自被审提交上 PR 自己的 CI(经 API 获取),本地未构建或执行任何代码。单测端到端钉住核心主张:接线测试两次驱动 parseArgsCommand.handler——先 --effort high(断言确切的 0o600/noFollow 写入),再无参数(断言 effort: higheffortSource: last_used 与复用警告)——另有优先级、强制、非法值、无替换的损坏状态、不可读状态、写失败、resume 形态各用例。套件之外,maintainer 两次驱动构建产物与 merge-base 做 A/B(第一轮 Linux、本 head 的复验在 macOS)——故障注入、软链种植、24 路并发,以及确认新 fail-open 测试确实可区分的变异测试——并已在该 SHA 上批准。两个 pull_request 工作流均为绿;macOS/Windows/集成测试脚位是仓库仅 merge_group 的面(按设计跳过)。

行为主张已由 2b(套件经真实 handler 钉住流程)加 maintainer 的两轮构建产物 A/B 证实,因此这次不需要沙箱验证行。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — the design was right in round 1, the one blocker raised since is closed with tests that discriminate, and the maintainer's built-binary re-verification at this exact head already went deeper than a typical merge.

Stepping back: my independent proposal for this feature — user-private per-project state, explicit > remembered > configured > default with the safety forcings on top, a visible reuse notice, fail-open I/O because the file sits on the review's critical path — is exactly what shipped. The first-round review passed the shape; the maintainer's round-1 A/B found the one real defect (fail-closed memory I/O able to kill a review over a file the user never asked for, invisibly — 0-byte verdict behind an exit-0 tee), and what landed since is the fix plus the regression tests he asked for, which I confirmed actually discriminate: removing either try/catch fails the PR's own suite. The write-side NOTE names the resolved level rather than the typed one, the malformed-without-replacement path falls through with a NOTE: exactly like the adjacent settings degrade policy, and the storage posture (0600 + forceMode + noFollow, 0700 parent, session-owner-aware path) is the same option set the codebase already trusts for user-private state. Every diff edit earns its place — code, SKILL.md contract, two docs files, and the generated schema move together — and the one disclosed behavior change besides the feature itself (resume passes configured through, resolving a genuine old ambiguity) is documented in both docs and called out by the maintainer as the right call.

After seeing it run — CI green on the reviewed commit, plus two built-binary A/B rounds with fault injection, symlink plants, and 24-way concurrency — the results match what the PR promised. If I were maintaining this in six months, the fail-open NOTEs and the pinned precedence tests are exactly what I'd want to find.

Three follow-ups for after merge, none blocking (all from the maintainer's re-verification, all explicitly deferred there): the FIFO/blocking-read + stderr-echo hardening (his measured one-line fix is ready), a supported reset path, and the file's location named in the docs.

Approving, pinned to the reviewed commit — CI is fully settled on it (both pull_request runs green) and the fork-refactor guardrail does not apply (cross-repo feat).

中文说明

置信度:5/5 —— 设计在第一轮就是对的,此后提出的唯一阻断项已闭合并配有可区分的测试,且 maintainer 在本 head 上的构建产物复验已深于一般合并标准。

退一步看:我为这个功能写的独立方案——用户私有的按项目状态、显式 > 记忆 > 配置 > 默认且安全强制居其上、复用时可见提示、fail-open I/O(该文件位于 review 关键路径)——正是最终落地的形态。第一轮审查通过了整体形状;maintainer 的第一轮 A/B 找到了唯一真实缺陷(fail-closed 的记忆 I/O 可能因一个用户从未要求的文件杀死整个 review,且不显眼——exit 0 的 tee 管道后是 0 字节判定文件),此后落地的正是修复加上他要求的回归测试,且我确认这些测试确实可区分:去掉任一 try/catch,PR 自己的套件就会失败。写侧 NOTE 写明的是解析后的档位而非输入的档位,无替换的损坏状态会带 NOTE: 回落——与相邻 settings 降级策略完全一致,存储姿态(0600 + forceMode + noFollow0o700 父目录、感知 session 归属的路径)与代码库对用户私有状态已有的信任选项集相同。每一处改动都物有所值——代码、SKILL.md 契约、两处文档与生成的 schema 同步推进——除功能本身外唯一有披露的行为变更(resume 透传 configured,消解一处真实的旧歧义)在两处文档中写明,maintainer 也认为取舍正确。

看它跑过之后——被审提交上 CI 全绿,外加两轮带故障注入、软链种植与 24 路并发的构建产物 A/B——结果与 PR 承诺一致。若六个月后由我维护,这些 fail-open NOTE 与钉住优先级的测试正是我希望看到的东西。

合并后的三个后续项,均不阻塞(皆出自 maintainer 复验、且已被其明确延后):FIFO/阻塞读 + stderr 回显的加固(他已实测的一行修复现成可用)、受支持的清除路径、以及在文档中写明该文件的位置。

批准,锚定在被审提交——其上的 CI 已完全落定(两个 pull_request 运行均为绿),fork-refactor 护栏不适用(跨仓 feat)。

Qwen Code · qwen3.8-max

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

中文说明

已审查——无阻断问题。 建议见行内评论。

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

Comment thread packages/cli/src/commands/review/parse-args.ts Outdated
Comment thread packages/cli/src/commands/review/parse-args.ts
Comment thread packages/core/src/skills/bundled/review/SKILL.md
Comment thread docs/users/configuration/settings.md
Comment thread packages/cli/src/commands/review/parse-args.ts
Comment thread packages/cli/src/commands/review/parse-args.ts
Comment thread packages/cli/src/commands/review/parse-args.ts

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

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

  • packages/cli/src/commands/review/parse-args.ts:1001 — [probe] R2-2 readLastReviewEffort's malformed-content branch has zero flag-less test coverage (both fallback mutations survive 150/150 green)
  • packages/cli/src/commands/review/parse-args.ts:1027 — [probe] R2-3 configured-rank non-persistence is untested; a persist-after-read mutation survives green and would permanently shadow review.effort
  • packages/cli/src/commands/review/lib/paths.test.ts:33 — [probe] R2-4 fallback-storage privacy assertions under-anchored; a workspace-leak mutation ships green
中文说明

已审查——无阻断问题。 建议见行内评论。

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

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

Comment thread packages/cli/src/commands/review/parse-args.ts Outdated
Comment thread packages/cli/src/commands/review/parse-args.ts Outdated
Comment thread packages/cli/src/commands/review/parse-args.ts
Comment thread packages/cli/src/commands/review/parse-args.ts Outdated

Copy link
Copy Markdown
Contributor Author

All current CI checks are green on a8c8268. The three actionable review suggestions were addressed in 985a7eb, the remaining suggestions have been answered, and all conversations are resolved. Could a code owner take the final review when convenient?

@wenshao

wenshao commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Local runtime verification of #9883 (real builds, real filesystem, A/B against the merge-base)

I built both arms locally and drove the built binary, not the unit suite: PR head a8c8268 and merge-base 65c2bb0, node packages/cli/dist/index.js review parse-args --stdin against an isolated QWEN_HOME and real project roots. Linux, Node 22.22.2.

The feature does exactly what the PR says it does, and the storage hygiene is the right posture — I attacked it and it held. One thing I would want changed before merge, and it is small.


1. The advertised behaviour reproduces, end to end

Steps 1–3 of the Reviewer Test Plan, verbatim, on the built CLI:

feature works

Also confirmed by direct probe, all on the built binary:

Probe Result
Cross-project isolation (two project roots) independent; projA=high, projB=default until it gets its own
Remembered outranks review.effort mem low + configured highlow / last_used; merge-base gives high / configured
--comment still forces high over remembered low high / forced-by-comment
--fix still floors at medium over remembered low medium / forced-by-fix
--effort low --comment (forced to high) verdict high, stored low — remembers what was typed, not what was forced ✅
--effort ultra (invalid) memory untouched, both warnings compose correctly ✅
--effort HIGH normalised, stored high
Target default alone never persisted ✅
QWEN_CODE_PROJECT_DIR set, cwd = PR worktree resolves to the session owner (medium/last_used); unset → worktree's own dir (high/default). The env var is load-bearing and correct ✅
Symlink planted at the memory path → /rl/victim.txt noFollow held: symlink replaced by a real 0600 file, victim untouched ✅
24 concurrent processes (12 writers alternating low/high + 12 readers) all exit 0, no torn read, no leftover *.tmp, final file well-formed ✅

--fix + --resume on a PR target is unreachable as a combination (--fix is ignored there, effortSource stays last_used), so forced-by-fix never needs a resume rule.

Suites, on the PR arm in my tree: CLI 195 (2 skipped) ✅, core skill 41/41 ✅, tsc --noEmit clean for both workspaces ✅, eslint --max-warnings 0 on all six changed .ts files ✅, generate:settings-schema produces no diff (the regenerated VS Code schema is in sync) ✅. The whole packages/cli/src/commands/review suite is 4980 passed / 1 failed — that one failure (run-ledger.test.ts › refuses to append over a ledger it could not read) fails identically on the merge-base build; it is a running-as-root artifact of my box, not this PR.


2. What I would change before merge: the memory is fail-closed, and it sits on the critical path

readLastReviewEffort throws on a value it does not like, writeLastReviewEffort lets atomicWriteFileSync throw, and the handler catches neither. The triage bot raised the read half as a non-blocking question. Having actually run it, I think the assessment moves, for three measured reasons.

corrupt state crashes the review

(a) The escape hatch is not as visible as it reads. The triage note says the error names the path and the valid values, so the fix is actionable. That is true if you run parse-args by hand. But SKILL.md Step 1 runs it as … parse-args --stdin < args | tee .qwen/tmp/qwen-review-parse-args.json, and tee swallows the status: the pipeline exits 0 and qwen-review-parse-args.json lands at 0 bytes. The skill's contract for that file is "use it verbatim … do not re-derive it" — there is no branch for an empty one, and nothing in the verdict path signals failure. The stack trace is on stderr, so the model does see text; what it does not see is a failure.

(b) The failure class is wider than a corrupt file. It is every I/O fault on either side, and the write half fires even when the user typed an explicit --effort — so "an explicit invocation self-heals it" does not cover write faults. Measured, all exit 1, all on the PR build, all exit 0 on the merge-base build under the identical condition:

Condition PR build merge-base
memory file holds foo (no --effort) Error: … must contain low, medium, or high high / default
memory file empty (no --effort) ✗ same high / default
a directory at the memory path (no --effort) EISDIR: … read
a directory at the memory path, --effort low EISDIR: … rename low / explicit
QWEN_HOME on a full filesystem, --effort high ENOSPC: no space left on device, write high / explicit

write failure and no reset

(c) The adjacent code in the same statement already rules the other way. parseArgsCommand.handler calls reviewDefaultsFromSettings() and parseReviewArgsWithMemory(...) on the same line. The first one degrades — operatorReviewSettings() catches, writes a NOTE: with writeStderrLineSafe, and returns the defaults, with a comment that spells out exactly this reasoning:

"An unreadable file is enough: this is read while a plan is being captured, the review's first step, so the throw would end the whole review over a permissions bit on a file none of these settings had to come from. Degrade to the defaults instead and say so."

That comment describes the new code's failure mode precisely — over a file that, unlike a settings file, the user never asked for and cannot see. Same handler, same statement, opposite policy.

No test covers any of this. The suite pins "explicit replaces malformed state" but never "malformed state without --effort", and node:fs is mocked throughout, so the I/O-fault class is structurally invisible to it. I confirmed this the direct way: I applied the fail-open patch below and the PR's own tests still pass, 150 passed | 2 skipped. A behaviour no test can distinguish is a behaviour nobody chose.

The fix, applied and verified on top of the PR (build clean, happy path byte-identical, every row above turns into a NOTE: and the pre-PR resolution):

suggested fix

function readLastReviewEffort(path: string): ReviewEffort | undefined {
  let value: string;
  try {
    if (!existsSync(path)) return undefined;
    value = readFileSync(path, 'utf8').trim();
  } catch (error) {
    writeStderrLineSafe(`NOTE: the remembered review effort could not be read (…); resolving from settings and defaults instead.`);
    return undefined;
  }
  const effort = asEffort(value);
  if (effort === null) {
    writeStderrLineSafe(`NOTE: ${path} must contain low, medium, or high; got ${JSON.stringify(value)}. Ignoring it; type \`--effort <level>\` to record a new one.`);
    return undefined;
  }
  return effort;
}
// and the same try/catch around mkdirSync + atomicWriteFileSync in writeLastReviewEffort

Worth a regression test for the two shapes the current suite cannot see: malformed content with no --effort, and a write that throws.


3. Two non-blocking observations

There is no supported way back to the built-in default. Once a level is remembered, --effort auto, --effort default and --effort none are all rejected as invalid values and the remembered level survives (measured — screenshot 3, bottom). review.effort: "auto" no longer restores anything either, since remembered outranks configured. The only exit is deleting ~/.qwen/projects/<sanitized-cwd>/review-last-effort by hand, and neither the docs nor any command mentions that path. The PR lists a clear/inspect UI as out of scope, which is fine — but accepting --effort auto as "forget the remembered level" would be a few lines and would close it. At minimum, the docs paragraph that says "type a new --effort to replace it" could say where the value lives.

--resume can now discard an interrupted run it previously continued. Driving the real assessResume with an interrupted run recorded at high:

RESUMED                    <- before: effortSource=default   -> skill omits --effort
REFUSED(effort-mismatch)   <- after:  effortSource=last_used(low)  -> skill passes --effort low
RESUMED                    <- after:  effortSource=last_used(high) -> skill passes --effort high

Sequence: start /review 9792 at the default high, it dies; type /review --effort low on some local change in the same project; come back with /review 9792 --resume → the remembered low is now a "required level", the resume is refused, and the interrupted high run's worktree and certified agent results are thrown away. It is disclosed (the reuse warning plus the effort-mismatch reason), and it follows from the PR's own principle, so I am not calling it wrong — but it is a new way to lose saved work that the PR body's "starts fresh at the level required by the current invocation" undersells. Worth one sentence in the docs.

Separately, credit where due: the SKILL.md resume enumeration (explicit / last_used / configured / forced-by-comment, omit only default) resolves a real pre-existing ambiguity — the old wording said "pass for explicit or forced-by-comment" and "omit ONLY when default", which left configured undecided.


Verdict

The design is right, the precedence table is right, the storage hygiene is genuinely good — 0600 + noFollow + atomic survived a symlink plant and 24-way concurrency without a scratch — and the docs and SKILL.md move together with the code. My recommendation is: merge after §2, the fail-open change. It is ~15 lines, it is already verified to keep every advertised behaviour intact, and it is the difference between "the review starts and tells you it ignored a stale file" and "the review does not start, with a 0-byte verdict and exit 0". §3 can be follow-ups.

Verification setup (for reproduction)
PR arm:        git worktree at pull/9883/head = a8c82686a6 , npm run build
Base arm:      git worktree at merge-base     = 65c2bb01c0 , npm run build
Driver:        node <arm>/packages/cli/dist/index.js review parse-args --stdin
Isolation:     QWEN_HOME=<lab>/home ; project roots are real git repos ; memory file at
               <QWEN_HOME>/projects/<sanitizeCwd(root)>/review-last-effort
Fault injection: 64K tmpfs filled to 100% for ENOSPC ; mkdir at the memory path for EISDIR ;
               symlink to a victim file for noFollow ; 24 concurrent processes for atomicity
Resume:        assessResume() from the built dist, driven directly with the three effortSource shapes

Note on my box: run-ledger.test.ts › refuses to append over a ledger it could not read fails on both arms because the tests run as root (mode 000 does not stop root from reading). Unrelated to this PR.

中文说明

#9883 的本地运行时验证(真实构建、真实文件系统、与 merge-base 做 A/B)

我在本地构建了两个 arm 并驱动构建产物而非单测:PR head a8c8268 与 merge-base 65c2bb0,用隔离的 QWEN_HOME 和真实项目根跑 node packages/cli/dist/index.js review parse-args --stdin。Linux,Node 22.22.2。

这个功能确实做到了 PR 所声称的一切,存储卫生的姿态也正确 —— 我做了攻击性探针,它扛住了。合并前我希望改一处,改动很小。

1. 声称的行为完整复现

Reviewer Test Plan 的第 1–3 步在构建后的 CLI 上逐字重现(见上方第 1 张截图)。另外直接探针确认:

探针 结果
跨项目隔离(两个项目根) 相互独立;projA=high,projB 在拿到自己的记忆前一直是 default
记忆值压过 review.effort 记忆 low + 配置 highlow / last_used;merge-base 给 high / configured
--comment 仍能压过记忆 low 强制 high high / forced-by-comment
--fix 仍能压过记忆 low 兜底到 medium medium / forced-by-fix
--effort low --comment(被强制成 high) 判定 high存储 low —— 记住的是用户输入的,不是被强制的 ✅
--effort ultra(非法值) 记忆不动,两条警告组合正确 ✅
--effort HIGH 归一化后存 high
仅目标默认值 从不持久化 ✅
设了 QWEN_CODE_PROJECT_DIR、cwd 在 PR worktree 解析到 session 归属(medium/last_used);不设则落到 worktree 自己的目录(high/default)。这个环境变量是承重的,且用法正确 ✅
在记忆路径上种一个指向 /rl/victim.txt 的软链 noFollow 生效:软链被替换成真实 0600 文件,victim 未被改写 ✅
24 个并发进程(12 写 low/high 交替 + 12 读) 全部 exit 0,无撕裂读,无残留 *.tmp,最终文件完好 ✅

PR target 上 --fix + --resume 的组合不可达(--fix 在那里被忽略,effortSource 保持 last_used),所以 forced-by-fix 不需要 resume 规则。

我这边的测试:CLI 195(2 skipped)✅、core skill 41/41 ✅、两个 workspace 的 tsc --noEmit 干净 ✅、6 个改动 .ts 文件 eslint --max-warnings 0 ✅、generate:settings-schema 无 diff(重新生成的 VS Code schema 已同步)✅。整个 packages/cli/src/commands/review 套件 4980 passed / 1 failed——那一条失败(run-ledger.test.ts › refuses to append over a ledger it could not read在 merge-base 上以同样方式失败,是我这台机器以 root 运行的产物,与本 PR 无关。

2. 合并前我建议改的一处:记忆是 fail-closed 的,而且位于关键路径上

readLastReviewEffort 遇到不认识的值就抛,writeLastReviewEffortatomicWriteFileSync 抛,handler 两个都没 catch。triage bot 已经把读的那半提成了非阻塞疑问。真跑过之后,我认为结论应该往前挪一格,有三条实测理由。

(a) 逃生门没有看上去那么显眼。 triage 说报错点明了路径和合法值,所以可操作。手动跑 parse-args 时确实如此。但 SKILL.md Step 1 的形态是 … parse-args --stdin < args | tee .qwen/tmp/qwen-review-parse-args.jsontee 吞掉了退出码:整条管道 exit 0,而 qwen-review-parse-args.json 落成 0 字节。skill 对这个文件的契约是"逐字使用……不要自行推导",对空文件没有分支,判定链路上也没有任何失败信号。堆栈在 stderr 上,所以模型确实看得到文本;它看不到的是"失败"。

(b) 失败面比"文件损坏"宽得多。 是两侧的每一种 I/O 故障,而且写的那一半即使用户显式输入了 --effort 也会触发——所以"显式调用可自愈"覆盖不到写故障。实测结果(PR arm 全部 exit 1,merge-base 在相同条件下全部 exit 0):

条件 PR 构建 merge-base
记忆文件内容为 foo(不带 --effort Error: … must contain low, medium, or high high / default
记忆文件为空(不带 --effort ✗ 同上 high / default
记忆路径是个目录(不带 --effort EISDIR: … read
记忆路径是个目录,--effort low EISDIR: … rename low / explicit
QWEN_HOME 在满盘文件系统上,--effort high ENOSPC: no space left on device, write high / explicit

(c) 同一条语句里的相邻代码早就给出了相反裁决。 parseArgsCommand.handler 在同一行上调用 reviewDefaultsFromSettings()parseReviewArgsWithMemory(...)。前者会降级——operatorReviewSettings() 有 catch,用 writeStderrLineSafe 打一条 NOTE: 然后返回默认值,注释把这条推理写得明明白白:

"An unreadable file is enough: this is read while a plan is being captured, the review's first step, so the throw would end the whole review over a permissions bit on a file none of these settings had to come from. Degrade to the defaults instead and say so."

这段注释精确描述了新代码的失败模式——而且这次的文件与 settings 文件不同:用户从没要求过它,也看不见它。同一个 handler、同一条语句、相反的策略。

这些路径没有任何测试覆盖。 套件钉住了"显式值替换损坏状态",却从没覆盖"损坏状态 且不带 --effort";而且 node:fs 全程被 mock,I/O 故障这一类在结构上对它不可见。我用最直接的方式确认了:把下面的 fail-open 补丁打上去,PR 自己的测试照样全过,150 passed | 2 skipped。测试无法区分的行为,等于没人选择过。

补丁已实际应用并验证(构建干净,happy path 逐字不变,上表每一行都变成一条 NOTE: 加上 PR 之前的解析结果)——代码见英文部分。建议为当前套件看不见的两种形态补回归测试:不带 --effort 的损坏内容,以及会抛的写入。

3. 两点非阻塞观察

没有受支持的办法回到内置默认值。 一旦记住了某个档位,--effort auto--effort default--effort none 都会被判为非法值而记忆值继续生效(已实测,见第 3 张截图下半部分)。review.effort: "auto" 也救不回来,因为记忆值压过配置。唯一出路是手工删掉 ~/.qwen/projects/<sanitized-cwd>/review-last-effort,而文档和任何命令都没提过这个路径。PR 把"查看/清除 UI"列为范围外,这没问题——但把 --effort auto 接成"忘掉记忆值"只要几行就能补上。最低限度,文档里"type a new --effort to replace it"那句可以顺便说明这个值存在哪。

--resume 现在可能丢弃它以前能续上的中断任务。 用构建产物里真实的 assessResume 驱动一个记录为 high 的中断任务:

RESUMED                    <- 之前: effortSource=default          -> skill 省略 --effort
REFUSED(effort-mismatch)   <- 之后: effortSource=last_used(low)   -> skill 传 --effort low
RESUMED                    <- 之后: effortSource=last_used(high)  -> skill 传 --effort high

场景:/review 9792 以默认 high 启动后中断;随后在同一项目里对本地改动敲了 /review --effort low;再回来 /review 9792 --resume —— 记忆的 low 此刻成了"本次调用要求的档位",resume 被拒,中断那次 high 运行的 worktree 和已认证的 agent 结果被丢弃。这件事是有披露的(复用警告 + effort-mismatch 原因),也符合 PR 自己的原则,所以我不认为它是错的——但它是一条新的"丢失已保存工作"的路径,PR 描述里"starts fresh at the level required by the current invocation"把它说轻了。文档里值得补一句。

另外该给的肯定:SKILL.md 的 resume 枚举(explicit / last_used / configured / forced-by-comment,仅 default 省略)解决了一处真实存在的旧歧义——旧措辞既说"explicit 或 forced-by-comment 才传",又说"仅 default 才省略",把 configured 悬在了中间。

结论

设计是对的,优先级表是对的,存储卫生是真的好——0600 + noFollow + 原子写在软链攻击和 24 路并发下毫发无损——文档和 SKILL.md 也和代码同步推进。我的建议是:改完 §2 后合并。 那是约 15 行,已验证不改变任何已声称行为,而它决定的是"review 正常开始并告诉你忽略了一个陈旧文件"还是"review 起不来,判定文件 0 字节,退出码 0"。§3 可以作为后续。

@tlysanhuo
tlysanhuo force-pushed the tlysanhuo/review-last-effort branch from a8c8268 to 8c4f0f7 Compare August 26, 2026 02:45
@tlysanhuo
tlysanhuo requested a review from qqqys as a code owner August 26, 2026 02:45
@github-actions

Copy link
Copy Markdown
Contributor

Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration.

中文

请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。

@tlysanhuo

Copy link
Copy Markdown
Contributor Author

Addressed on 8c4f0f7d8 after rebasing the branch onto current main (31ad20bef).

  • readLastReviewEffort now catches read failures and treats malformed content as invalid remembered state: both paths emit a NOTE: on stderr and continue resolution from review.effort and the target default.
  • writeLastReviewEffort now catches directory/atomic-write failures, emits a NOTE:, and preserves the explicitly requested effort for the current review.
  • Added handler-level regressions for malformed remembered content without an explicit replacement and for an ENOSPC persistence failure.
  • Resolved the upstream conflicts while retaining the newer topology and adaptive severity-floor behavior from main.

Validation on the rebased head:

  • CLI focused tests: 214/214
  • Core review skill tests: 42/42
  • CLI and Core typechecks: pass
  • Prettier and ESLint on the changed TypeScript files: pass
  • The full build completed Core, all channel packages, SDK, CLI, schema generation, and Web UI. It later stopped in the unrelated Web Shell stage because the host-level /Users/tly/postcss.config.js imports a locally unavailable tailwindcss module.

I kept reset semantics (--effort auto) out of this PR as the non-blocking follow-up discussed above.

@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. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.

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

中文说明

已审查——无阻断问题。 建议见行内评论。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。

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

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

Comment thread packages/cli/src/commands/review/parse-args.ts Outdated
Comment thread packages/cli/src/commands/review/parse-args.ts
Comment thread packages/cli/src/commands/review/parse-args.ts
Comment thread packages/cli/src/commands/review/parse-args.ts
Comment thread packages/cli/src/commands/review/parse-args.ts
@tlysanhuo

Copy link
Copy Markdown
Contributor Author

Addressed the round-3 review suggestions in 215006780 and resolved all review threads.

Local verification: focused CLI suite 154/154, CLI typecheck, ESLint, Prettier, and git diff --check all pass. CI is re-running on the new head; once green, this is ready for final code-owner review.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

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

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

  • packages/cli/src/commands/review/lib/paths.test.ts:32 — [probe] fallback-storage privacy test under-guards its property — a workspace-local fallback survives all assertions (re-discovery of R2-4, deferred round 2)
  • packages/cli/src/commands/review/lib/paths.ts:69 — [probe] settings-relocated runtime base fragments the effort memory between in-session and standalone invocations

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

中文说明

已审查——无阻断问题。 建议见行内评论。

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

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

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

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

});

if (explicitEffort !== undefined) {
writeLastReviewEffort(effortPath, explicitEffort, initial.effort);

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] No test pins which value is persisted when a safety floor overrides the explicit effort. The write call persists the typed explicitEffort — correct per the documented "last level explicitly typed" contract — while initial.effort is only used for the failure NOTE, and the one test combining a forcing with the write path (reports the resolved effort when remembering an explicit effort fails, 6711 --comment --effort low) makes atomicWriteFileSync throw, so nothing observes the persisted value.

A probe at the reviewed commit confirms the gap: a mutant persisting the forced level (writeLastReviewEffort(effortPath, initial.effort, initial.effort)) passes the whole suite 154/154, because the ENOSPC test asserts only stderr fragments the mutant also produces. If that mutant ever ships, after one /review 6711 --comment --effort low every later flag-less review in the project reuses high instead of the level the user typed, contradicting the contract this PR documents in three places.

Add one wiring test that observes a successful write under forcing:

it('persists the typed effort when a safety floor overrides it', async () => {
  const storedEffort = lastReviewEffortPath(
    process.cwd(),
    process.env['QWEN_CODE_PROJECT_DIR'],
  );

  const got = await verdictFor('6711 --comment --effort low\n');
  expect(got.effort).toBe('high');
  expect(got.effortSource).toBe('forced-by-comment');
  expect(fsState.written.get(storedEffort)).toBe('low\n');
});

The new test must go red if the persisted argument at this write site is ever replaced with initial.effort — verify by running the mutation (the stored file becomes high\n instead of low\n).

中文说明

[建议] 目前没有任何测试钉住:当安全底线覆盖显式 effort 时,持久化的到底是哪个值。写入调用持久化的是用户显式输入的 explicitEffort——这是正确的,符合文档中"最后一次显式输入的档位"契约——而 initial.effort 仅用于失败提示;并且唯一同时覆盖强制底线与写入路径的测试(reports the resolved effort when remembering an explicit effort fails6711 --comment --effort low)让 atomicWriteFileSync 抛错,因此没有任何测试观察到持久化的值。

在被审提交上的探针证实了这个缺口:把持久化值改为强制档位的变异体(writeLastReviewEffort(effortPath, initial.effort, initial.effort))仍能通过全部 154/154 个测试,因为 ENOSPC 测试只断言了变异体同样会输出的 stderr 片段。若该变异体将来被引入,那么一次 /review 6711 --comment --effort low 之后,该项目后续所有不带参数的 review 都会复用 high 而不是用户输入的档位,与本 PR 在三处文档中写明的契约矛盾。

请补一个观察强制底线写入成功路径的测试(代码见上方英文部分)。若此写入位点的持久化参数将来被替换为 initial.effort,新测试必须变红——可运行该变异(持久化文件变为 high\n 而非 low\n)来验证。

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

@wenshao

wenshao commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Re-verification of #9883 at 21500678 — the blocker I raised is closed

Second local run against the current head, not the unit suite: PR head 2150067809 and merge-base a770aefd each built from scratch (npm ci + npm run build), driven as node packages/cli/dist/index.js review parse-args --stdin against isolated QWEN_HOMEs and real project roots. macOS 15, Node 24.18.1. My previous report was against a8c8268; three rounds of changes have landed since.

Recommendation: merge. Everything I asked for in §2 last time is in, and it is pinned by tests that actually discriminate. What remains is follow-up material.


1. §2 — fail-open: closed

Every condition that exited 1 on a8c8268 now exits 0, resolves identically to the merge-base, and says why on stderr.

fail-open fixed

The §2(a) half is closed too. Under SKILL.md's Step 1 shape — … parse-args --stdin | tee .qwen/tmp/qwen-review-parse-args.json — a corrupt or unusable remembered state used to leave a 0-byte verdict behind an exit-0 pipeline. It now lands a well-formed 488-byte verdict identical to the merge-base's, so the skill's "use it verbatim" contract holds under every fault I could produce.

2. The new branches are covered by tests that can tell the difference

My sharpest complaint last time was that no test could distinguish fail-open from fail-closed, so nobody had chosen. I checked that directly by mutating the shipped code and running the PR's own suite:

mutation PR's suite caught by
drop the try/catch in readLastReviewEffort 1 failed / 153 passed ignores unreadable remembered state
drop the try/catch in writeLastReviewEffort 1 failed / 153 passed reports the resolved effort when remembering an explicit effort fails

Round 3's diagnostic fix also reproduces on the built binary — the write-failure NOTE names the resolved level, not the typed one, on all three forcing paths (--comment → high, --fix → medium, neither → low).

3. Feature and storage matrix — no regressions

feature matrix

Precedence, cross-project isolation, both forcings, "remember what was typed not what was forced", invalid-value containment, non-persistence of default/configured, QWEN_CODE_PROJECT_DIR ownership, the symlink plant (noFollow holds; victim untouched; replaced by a real 0600 file) and 24-way concurrency (no torn read, no leftover .tmp) all behave as documented.

I also checked the double-parse in parseReviewArgsWithMemory for the obvious hazard: parseReviewArgs has a single exit and writes nothing to stderr, so the second pass cannot duplicate a side effect. Measured on a run that produces three warnings: multiset size 3, unique 3.

Gates on my tree: tsc --noEmit clean for both workspaces; eslint --max-warnings 0 clean on all six changed files; focused tests 216/216 (CLI) and 42/42 (core skill); the whole packages/cli/src/commands/review suite 5117 passed / 2 skipped / 0 failed; generate:settings-schema produces no diff. (One [vitest-worker]: Timeout calling "onTaskUpdate" surfaced on the full run with every test green and exit 0 — the known worker-RPC flake, unrelated.) Last time one run-ledger test failed identically on both arms as a running-as-root artifact of my box; not running as root, it passes.

The docs moved with the behaviour, including the resume sentence I asked for.


4. Still open — all non-blocking, none of it should hold the merge

remaining observations

(a) The read path is fail-open for errors, but not for a blocking file. existsSync answers yes for a FIFO; readFileSync then waits for a writer that never comes. A try/catch cannot see this — there is no error and no timeout. With a FIFO at the memory path and no --effort, the PR build hangs forever where the merge-base exits 0. Separately, got ${JSON.stringify(value)} echoes the whole unusable file to stderr (200 KB in → 64 KB out on one line), and the read side follows symlinks, so a link planted there puts another file's bytes into the review model's context.

This needs same-user write access to a 0700 directory, so it is hardening rather than a privilege boundary — but this exact path already carries noFollow against that threat model, which is why I think it is worth one line. Both close together, and I measured it after applying and rebuilding:

- if (!existsSync(path)) return undefined;
+ if (!statSync(path, { throwIfNoEntry: false })?.isFile()) return undefined;

plus .slice(0, 40) inside the JSON.stringify. FIFO → exit 0, high/default; a directory at the path stops emitting EISDIR noise and skips quietly; corrupt, unreadable, happy-path and symlink-plant behaviour all unchanged; tsc clean. Five unit tests go red only because the suite's node:fs mock supplies existsSync/readFileSync but no statSync — which is itself the reason this class was invisible: the remembered file lives in an in-memory map, so real-filesystem object types never reach the code under test.

(b) Still no supported way back to the built-in default. --effort auto|default|none|reset are all rejected as invalid and the remembered level survives; review.effort: "auto" no longer restores anything either. The only exit remains deleting ~/.qwen/projects/<sanitized-cwd>/review-last-effort by hand, and the docs still do not name that path. Explicitly deferred, and I agree it can be — but the docs line that says "type a new --effort to replace it" is one clause away from also saying where the value lives.

(c) --resume can discard an interrupted run, now including the configured case. Driving the real assessResume against an interrupted run recorded at high, with every other probe intact, the table in the screenshot is what happens. The last_used(low) row is the one I flagged last time and it is now disclosed in the docs, so I am not re-raising it. Worth naming, though: the configured(low) row is a behaviour change that arrives with this PR but is not part of the memory feature. The old SKILL.md said both "pass for explicit or forced-by-comment" and "omit ONLY when default", leaving configured genuinely undecided; this PR resolves the contradiction toward passing. I think that is the right call — a configured level is user intent — and it only bites someone who changes review.effort mid-review, but it is a resume-semantics change riding along with an effort-memory PR.


Verdict

The design was right the first time; what changed since a8c8268 is that the storage is now honestly fail-open and the behaviour is pinned by tests that can tell the difference. §4 is all follow-up: (a) is a one-line hardening I have already measured, (b) is the reset UI the PR explicitly scoped out, (c) is disclosed. LGTM from me on the runtime behaviour.

Reproduction setup
PR arm     git worktree at pull/9883/head = 2150067809 , npm ci && npm run build
Base arm   git worktree at merge-base     = a770aefddd , npm ci && npm run build
driver     node packages/cli/dist/index.js review parse-args --stdin
isolation  fresh QWEN_HOME + fresh git-init'd project root per probe
memory     $QWEN_HOME/projects/<sanitizeCwd(cwd)>/review-last-effort  (0600)
resume     assessResume() imported from the built dist, synthetic PreviousReport/ResumeProbes
mutations  edit src, rebuild the cli workspace, re-run; source restored and rebuilt after
host       macOS 15 (arm64), Node v24.18.1, not root
中文说明

#988321500678 的复验 —— 我提的阻断项已闭合

这是第二轮本地实跑,针对当前 head,而不是跑单测:PR head 2150067809 与 merge-base a770aefd 各自从零构建(npm ci + npm run build),以 node packages/cli/dist/index.js review parse-args --stdin 驱动,每个探针配独立的 QWEN_HOME 和真实项目根目录。macOS 15,Node 24.18.1。我上一份报告针对的是 a8c8268,之后又落了三轮改动。

结论:可以合并。 我上次 §2 要求的都已到位,而且被真正有辨别力的测试钉住了。剩下的都属于后续项。

1. §2 —— fail-open:已闭合

a8c8268 上会 exit 1 的每一种情形,现在都 exit 0,解析结果与 merge-base 完全一致,并在 stderr 说明原因(见第一张图)。

§2(a) 那一半也闭合了。在 SKILL.md Step 1 的真实形态 … parse-args --stdin | tee .qwen/tmp/qwen-review-parse-args.json 下,损坏或不可用的记忆状态过去会在 exit 0 的管道后面留下一个 0 字节的 verdict;现在落盘的是与 merge-base 完全相同的 488 字节合法 JSON,所以 skill "逐字使用该文件" 的契约在我能造出的每一种故障下都成立。

2. 新增分支确实被有辨别力的测试覆盖

我上次最尖锐的一条是:没有任何测试能区分 fail-open 和 fail-closed,也就是说没人真正做过这个选择。我直接对发布代码做了变异,再跑 PR 自己的套件:

变异 PR 套件结果 被哪条测试抓住
去掉 readLastReviewEfforttry/catch 1 failed / 153 passed ignores unreadable remembered state
去掉 writeLastReviewEfforttry/catch 1 failed / 153 passed reports the resolved effort when remembering an explicit effort fails

round 3 的诊断修复在构建产物上也复现了:写失败的 NOTE 报告的是最终生效的档位而非输入值,三条强制路径都正确(--comment → high、--fix → medium、无强制 → low)。

3. 功能与存储矩阵 —— 无回归

优先级、跨项目隔离、两种强制、"记住输入值而非强制值"、无效值不污染记忆、默认值/配置值不持久化、QWEN_CODE_PROJECT_DIR 归属、符号链接植入(noFollow 生效,被指向的文件未被改动,链接被替换为真实 0600 文件)、24 路并发(无撕裂读、无残留 .tmp)均与文档一致(见第二张图)。

我也检查了 parseReviewArgsWithMemory 里两次解析的明显风险:parseReviewArgs 只有单一出口且不写 stderr,第二趟无法重复任何副作用。在一次产生三条 warning 的调用上实测:多重集大小 3,去重后 3。

我这边的门禁: 两个 workspace 的 tsc --noEmit 干净;六个改动文件 eslint --max-warnings 0 干净;定向测试 216/216(CLI)与 42/42(core skill);整个 packages/cli/src/commands/review 套件 5117 通过 / 2 跳过 / 0 失败generate:settings-schema 无 diff。(全量跑出现一次 [vitest-worker]: Timeout calling "onTaskUpdate",但所有测试为绿且退出码 0 —— 已知的 worker RPC 抖动,与本 PR 无关。)上次有一个 run-ledger 测试在两臂上同样失败,那是我机器以 root 运行导致的;非 root 下通过。

文档与行为同步更新,包括我上次要求补的 resume 那句。

4. 仍然开放的问题 —— 全部非阻断,都不应该拦合并

(a) 读路径对错误 fail-open,但对会阻塞的文件不是。 existsSync 对 FIFO 返回 true,readFileSync 随后会一直等待永远不来的写端。try/catch 看不见这种情况 —— 既没有错误也没有超时。在记忆路径放一个 FIFO 且不带 --effort 时,PR 构建永久挂起,而 merge-base exit 0。另外,got ${JSON.stringify(value)} 会把整个不可用文件回显到 stderr(输入 200 KB → 输出 64 KB 一整行),而且读路径会跟随符号链接,因此在那里植入一个链接就能把另一个文件的字节送进 review 模型的上下文。

这需要对一个 0700 目录有同用户写权限,所以属于加固而非权限边界 —— 但正是这条路径已经带了 noFollow 来防同一威胁模型,所以我认为值得改这一行。两个问题一起关掉,我已经应用并重新构建后实测:

- if (!existsSync(path)) return undefined;
+ if (!statSync(path, { throwIfNoEntry: false })?.isFile()) return undefined;

外加在 JSON.stringify 里补 .slice(0, 40)。实测:FIFO → exit 0, high/default;路径上是目录时不再产生 EISDIR 噪声而是安静跳过;损坏、不可读、正常路径和符号链接植入的行为全部不变;tsc 干净。有 5 个单测变红,仅仅是因为套件的 node:fs mock 提供了 existsSync/readFileSync 却没有 statSync —— 而这恰恰解释了这一类问题为什么此前不可见:记忆文件活在一个内存 map 里,真实文件系统的对象类型根本到不了被测代码。

(b) 仍然没有回到内置默认值的受支持路径。 --effort auto|default|none|reset 全部被判为无效值,记忆值原样保留;review.effort: "auto" 也不再能恢复什么。唯一的出口仍是手工删除 ~/.qwen/projects/<sanitized-cwd>/review-last-effort,而文档仍未提到这个路径。这一项已被明确列为后续,我同意可以后续做 —— 但文档里"输入新的 --effort 即可替换"那句,离顺带说明值存在哪里只差一个从句。

(c) --resume 可能丢弃中断的运行,现在还包括 configured 的情形。 用真实的 assessResume 驱动一个记录为 high 的中断运行、其余探针全部完好,结果见第三张图。last_used(low) 那一行是我上次提的,现在已在文档中披露,我不再重提。但值得点名的是:configured(low) 这一行是随本 PR 到来、却不属于记忆功能的行为变更。旧版 SKILL.md 同时写着"explicitforced-by-comment 时传递"和"仅当 default 时省略",configured 处于真正未决状态;本 PR 把这个矛盾消解为"传递"。我认为方向是对的 —— 配置的档位属于用户意图 —— 而且只会影响在 review 中途改 review.effort 的人,但它毕竟是一个搭在 effort 记忆 PR 上的 resume 语义变更。

结论

设计从一开始就是对的;相对 a8c8268 真正变化的是:存储现在诚实地 fail-open,且行为被有辨别力的测试钉住。§4 全部属于后续:(a) 是我已经实测过的一行加固,(b) 是 PR 明确划到范围外的重置能力,(c) 已有披露。就运行时行为而言,我这边 LGTM。

@wenshao

wenshao commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge August 27, 2026 12:46
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 27, 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: 78 passed · 0 failed · 78 total

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

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

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

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

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

Verification report

PR 9883 Deep Verification — feat(review): remember explicit effort per project

Verdict: merge-ready — 78/78 scripted assertions passed (65 A/B behavior cells, 8 mutation kills, 4 gate runs, 1 artifact-regeneration check). No blocking findings.
Verified head: 215006780990dfd9223f15aa29e0a545df780af1 (matches headRefOid), merged over base tip a0784a28305559260610cdb00a5deaf5ed15163e (merge-ref checkout, depth 2).

中文摘要
  • 结论merge-ready。78/78 脚本化断言通过;未发现阻塞性问题。
  • A/B 结论:中心主张被证明是承载性的(load-bearing)。head 构建在 --effort medium 9792 之后再次运行 9792 时返回 medium / last_used 并输出复用提示;同场景下 base 构建返回 high / default 且无任何提示(见「中央主张与 A/B」表,图 01-ab-head-vs-base.png)。base 全程未写出任何记忆文件(整臂扫描为 0 个)。解析优先级 显式 > 记忆 > review.effort > 目标默认 实测成立;三种失败路径(状态损坏、读取失败、写入失败)均按 fail-open + stderr NOTE 实测成立;记忆文件为原子写入、模式 0600、无 QWEN_CODE_PROJECT_DIR 时落入 Storage 项目目录。
  • Findings:仅 1 条低优先级观察项(目标被拒绝为非法 URL 时,显式档位仍会被记住,复用时有提示披露)和 1 条说明项(描述中的 resume 行为由 SKILL.md 指引 + parse-args 输出实现,非 fetch-pr 代码改动,已核实一致)。
  • 未覆盖范围:逐 commit 归因(浅克隆仅可达 1/6 个 commit,验证的是聚合 diff)、真实 PR 的 resume 端到端(沙箱无网络/凭据)、Windows 行为、仓库级全量测试、模型实际转达提示的交互链路。

Central claim + A/B

Central claim. An explicitly typed /review --effort <level> persists per project (user-private, atomic, mode 0600); a later invocation without --effort reuses it as effortSource: "last_used" with a relay notice; resolution order is explicit > remembered > operator review.effort > target default; I/O failures are fail-open and visible.

Control. Base worktree at HEAD^1 (a0784a2), only @qwen-code/qwen-code-core + @qwen-code/qwen-code rebuilt there; workspace links re-pointed into the base tree and realpath-asserted (readlink -f tmp/base-tree/node_modules/@qwen-code/qwen-code-coretmp/base-tree/packages/core; same for qwen-code). The PR leaves package.json/package-lock.json untouched, so reusing root externals is a clean control; the core diff is SKILL.md text only. Both arms drove the real built CLI headlessly: printf '<raw>\n' | HOME=… QWEN_HOME=… QWEN_CODE_PROJECT_DIR=… node <dist>/index.js review parse-args --stdin --out …. Harness: ab-harness.mjs; raw per-cell results: runs/results.json. Witness: evidence/01-ab-head-vs-base.png.

# scenario (fresh owner unless noted) HEAD result BASE result assertion outcome
c1 --effort medium 9792 medium/explicit, file medium\n medium/explicit, no file both as predicted
c2 9792 (chained after c1) medium/last_used + “reusing medium, the level you typed last time” high/default, silent, no file the flip
c3 planted medium\n + settings review.effort=low9792 medium/last_used (remembered outranks configured) low/configured both as predicted
c4 settings review.effort=low only → 9792 low/configured low/configured parity control
c5 planted garbage\n9792 high/default + stderr NOTE must contain low, medium, or high; got "garbage", file left in place high/default, no NOTE both as predicted
c6 planted garbage\n--effort low 9792 low/explicit, file overwritten low\n, no NOTE low/explicit, planted file untouched both as predicted
c7 owner dir chmod 500 → --effort high 9792 high/explicit + NOTE could not be remembered … this review still uses high high/explicit, no NOTE both as predicted
c8 no QWEN_CODE_PROJECT_DIR--effort high 9792 exactly one file at <QWEN_HOME>/projects/<hash>/review-last-effort, content high\n, mode 0600 nothing anywhere under QWEN_HOME/HOME both as predicted
c9 planted low\n chmod 000 → 9792 high/default + NOTE could not be read (EACCES…) high/default, no NOTE both as predicted

Head-only sibling sweep (all passed): --effort HIGH stored normalized high\n (h1); --effort=low equals-form stored (h2); --effort bogus keeps prior memory high\n unwritten and warns (h3); --effort low --effort medium stores medium\n — last valid wins (h4); --comment --effort low 9792 runs high/forced-by-comment but stores the typed low\n (h5, matches the documented “explicit value is remembered” intent); --resume 9792 with planted medium\nmedium/last_used, resume.effective: true (h6); explicit equal to target default still remembered (h8). Base global control: after removing the files I planted, zero review-last-effort files exist anywhere in the entire base arm.

Verdict is the pair of counts: on the reuse scenario head flips from base’s silent high/default to disclosed medium/last_used; base never writes the memory file, head writes it with the specified content, mode, and fallback path. 65/65 scripted cells.

Corrections

None to the code. One description clarification, recorded so the next reader is not misled: the body’s “Resume handling also passes remembered and configured levels to fetch-pr” is not a fetch-pr.ts code change — fetch-pr.ts is untouched by this PR. The mechanism is: parse-args now reports effortSource: last_used/configured, and the updated SKILL.md resume guidance passes --effort for every source except default; the receiving end (assessResumerequestedEffort: args.effort ?? null, effort-mismatch refusal) is pre-existing code traced at fetch-pr.ts:704 and fetch-pr.ts:745-762. I verified the two halves are consistent (see Findings #2 and Not covered).

Findings

1. (Low, observation) An explicit effort is persisted even when the target is refused. The remember callback fires at the end of parseReviewArgs, after warnings are composed but independent of target validity. Repro:

D=$(mktemp -d) && printf -- '--effort low https://github.com/foo/bar/pull/42oops\n' | \
  QWEN_CODE_PROJECT_DIR="$D" node packages/cli/dist/index.js review parse-args --stdin | grep effortSource
ls "$D"   # review-last-effort exists with "low\n"

Observed: verdict is explicit/low with the “Unrecognized URL … refusing to guess a target” warning, and low is stored anyway — so a typo’d-URL invocation records a level that a later, unrelated review in the same project reuses. Mitigating: the reuse is always disclosed by the last_used notice, and the PR’s stated rule is to remember what the user typed; this is a design judgment call, not a defect — recording it because it is the one door in the “parser/writer” surface where the stored intent did not produce a review.

2. (Info) Resume continuity claim verified at the level the sandbox allows. last_used and configured resolutions now ride the SKILL.md “pass --effort unless default” list; the mutated list (dropping last_used/configured) turns the new SKILL.test assertion red (mutant M6), and the parse-args side reports resume.effective: true together with last_used (cell h6). Both halves line up; the live fetch-pr mismatch cell needs a real PR (Not covered).

Not covered

  • Per-commit attribution. The metadata snapshot lists 6 commits; the depth-2 merge-ref checkout reaches only HEAD^2 (git rev-list HEAD^1..HEAD^2 = 1, repo is shallow). I verified the aggregate HEAD^1..HEAD diff (10 files, +478/−50) and state here that per-commit behavior attribution was out of reach.
  • Live resume E2E against a real PR (fetch-pr --resume refusing on effort mismatch and starting fresh): needs network + credentials this sandbox has none of. Verified instead: parse-args output (h6), fetch-pr requestedEffort plumbing by code trace, guidance pinned by tests (M6).
  • Windows behavior. The PR marks Windows ⚠️; the 0o600/0o700 mode bits are advisory there. This Linux container is the lane’s own runtime; no Windows arm available.
  • Repo-wide test suite — targeted suites only (below); the interactive skill flow where the model actually relays the notice (pinned only at text level by SKILL.test.ts); real --comment posting; review.effort from System/SystemDefaults scopes (User scope exercised via QWEN_HOME).

Targeted gates (all green, captured in evidence/03-focused-gates.png)

  • packages/cli: parse-args.test.ts (154) + lib/paths.test.ts (17) + settingsSchema.test.ts (47) → 218/218 passed.
  • packages/core: SKILL.test.ts42/42 passed.
  • tsc --noEmit clean for both @qwen-code/qwen-code and @qwen-code/qwen-code-core.
  • Committed generated artifact: re-ran npm run generate:settings-schema; regenerated settings.schema.json is byte-identical to the committed one.
  • Gate liveness: proven by the mutation matrix’s positive control below.

Mutation matrix (8/8 killed, no survivors; witness evidence/02-mutation-matrix.png)

mutant what it reverts suite result
M0 (positive control) notice wording “reusing” → “recycling” cli parse-args KILLED (5 red)
M1 last_used resolution branch cli parse-args KILLED (8 red)
M2 remember-callback invocation cli parse-args KILLED (6 red)
M3 reuse-notice warnings.push block cli parse-args KILLED (5 red)
M4 malformed-state stderr NOTE (silent fall-through) cli parse-args KILLED (1 red)
M5 noFollow: true on the atomic write cli parse-args KILLED (1 red)
M7 handler bypasses parseReviewArgsWithMemory entirely cli parse-args KILLED (9 red)
M6 SKILL.md resume list drops last_used/configured core SKILL.test KILLED (1 red)

M4’s red is the intended behavioral assertion: ignores malformed remembered state when no explicit effort replaces it fails with writeStderrLineSafe receiving no call (expect(...mock.calls[0]?.[0]).toContain(…) on undefined), i.e. the missing NOTE, not an import/compile break. Every mutant was applied to the pristine source by exact-anchor replacement and restored afterward (git status clean after each; scripted kill-assertion wrapper: mutation-assert.mjs, 8/8 PASS). The new tests are not vacuous, and no guard the PR introduces is dead or redundant.

Methodology

Environment: the CI verify container (node v22.23.2, linux, merge-ref checkout pull/9883/merge at depth 2; npm ci + npm run build pre-done at HEAD). The A/B drove the real built dist of both arms through review parse-args --stdin as child processes with isolated HOME/QWEN_HOME/QWEN_CODE_PROJECT_DIR scratch dirs and real file-mode checks (no mocks); settings injection used QWEN_HOME/<dir>/settings.json (User scope). The base arm rebuilt core+cli in a tmp/base-tree worktree after re-pointing node_modules/@qwen-code/* symlinks into it and asserting their realpath; a first base build attempt failed on ajv/dist/2020.js, traced to missing nested per-package node_modules links in the worktree (fixed by symlinking them; root lockfile untouched by the PR). Mutations were exact-string reverts applied one at a time with git checkout-equivalent restoration, each followed by the focused vitest suite. Raw logs: runs/results.json (all 65 A/B cell outcomes), mutation-runner.mjs/mutation-assert.mjs outputs (matrix), runs/ per-arm scratch. Evidence images were produced by scripts/verify-capture.mjs.

Flakiness gate log

rounds=5 files=3 skipped=0
file packages/cli/src/commands/review/lib/paths.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/review/lib/paths.test.ts
file packages/cli/src/commands/review/parse-args.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/review/parse-args.test.ts
file packages/core/src/skills/bundled/review/SKILL.test.ts: (cd packages/core) npx --no-install vitest run ./src/skills/bundled/review/SKILL.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/commands/review/lib/paths.test.ts: PPPPP
  packages/cli/src/commands/review/parse-args.test.ts: PPPPP
  packages/core/src/skills/bundled/review/SKILL.test.ts: PPPPP

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

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/commands/review/lib/paths.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/review/parse-args.test.ts: P (exit 0)
round 1 · packages/core/src/skills/bundled/review/SKILL.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/review/lib/paths.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/review/parse-args.test.ts: P (exit 0)
round 2 · packages/core/src/skills/bundled/review/SKILL.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/review/lib/paths.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/review/parse-args.test.ts: P (exit 0)
round 3 · packages/core/src/skills/bundled/review/SKILL.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/review/lib/paths.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/review/parse-args.test.ts: P (exit 0)
round 4 · packages/core/src/skills/bundled/review/SKILL.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/review/lib/paths.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/review/parse-args.test.ts: P (exit 0)
round 5 · packages/core/src/skills/bundled/review/SKILL.test.ts: P (exit 0)

Evidence images

01-ab-head-vs-base

02-mutation-matrix

03-focused-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 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot qwen-code-ci-bot added category/cli Command line interface and interaction priority/P2 Medium - Moderately impactful, noticeable problem scope/commands Command implementation type/enhancement Non-bug improvement or optimization labels Aug 27, 2026
@wenshao
wenshao added this pull request to the merge queue Aug 27, 2026
Merged via the queue into QwenLM:main with commit f9df444 Aug 27, 2026
257 checks passed
qwen-code-dev-bot added a commit to qqqys/qwen-code that referenced this pull request Aug 27, 2026
One conflict, in review/lib/paths.ts: both sides grew the import
block — main added `Storage` for QwenLM#9883's lastReviewEffortPath, this
branch added the fs/path/crypto imports for the generated-workflow
dir helpers. Resolution is the union: one combined core import, both
sides' functions kept untouched.

client.telemetrySwap.test.ts resolved itself: this branch's
48a01fa and main's 6dfb5a1 (QwenLM#10252) are byte-identical fixes
for the same TS1117 duplicate getToolRegistry.

Verified: npm run build, npm run typecheck, npm run lint all clean;
packages/cli src/commands/review 105 files / 5249 passed; packages/core
client.telemetrySwap.test.ts 10 passed.
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.22.3.

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

Labels

category/cli Command line interface and interaction priority/P2 Medium - Moderately impactful, noticeable problem scope/commands Command implementation type/enhancement Non-bug improvement or optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/review: remember the last explicitly typed --effort and reuse it with a notice

4 participants