Skip to content

feat(review): Test Plan claim check, base-tree A/B harness, per-hunk probes - #8215

Merged
wenshao merged 16 commits into
mainfrom
feat/review-borrowed-verification
Aug 1, 2026
Merged

feat(review): Test Plan claim check, base-tree A/B harness, per-hunk probes#8215
wenshao merged 16 commits into
mainfrom
feat/review-borrowed-verification

Conversation

@wenshao

@wenshao wenshao commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Gives /review three verification capabilities it could previously only approximate by reading, all borrowed from the shape of a hands-on maintainer verification (the workflow behind #8176's verification comment):

  1. Test Plan claim checking (qwen review test-plan). The PR description's Test Plan is a list of falsifiable assertions the author already wrote down, and nothing in the pipeline read it. The new subcommand extracts the checkable kinds — paths, npm scripts, test counts — and rules on each against the reviewed tree. A path that is in neither the diff nor the worktree, or a script no workspace manifest defines, is contradicted; a test count that differs from what this review's suites reported is deliberately only differs, never a contradiction, because the claim and the observation may be about different suites. Rulings are disclosed in the review body on every verdict (including Approve) and never block or cap — the deferred-checker precedent.
  2. A base-tree A/B harness (qwen review base-tree). Until now mergeBaseSha was used for exactly one thing — choosing the diff range — and no step ever built the code the PR is a change to, so every claim of the form "this changes/preserves observable behavior X" was settled by reading the diff. The new subcommand builds the merge base in a sibling worktree (reusing build-test's minimal build set and widening loop, plus a new --build-only mode), and the verifier's brief now offers it for comparative claims: run the same input in both trees and quote both outputs. It is spent per finding, not per review, and an unavailable base (no merge base, a possibly-stale one, a base that does not compile) is reported as a harness fact, never as a finding against the PR.
  3. Per-hunk red/green probing in test-efficacy. The revert probe is all-or-nothing and the statement mutants only cover a narrow safety-verb set, so a diff of condition/return-value changes — most diffs — produced zero mutants and no per-change signal. The third probe kind reverse-applies one hunk at a time and re-runs the affected tests, attributing a still-green suite to the specific change nothing gates. It inherits the mutants' discipline: compile failure is inconclusive (never killed), restore is by content, capped and budget-skipped counts are always reported.

Two supporting fixes came out of live verification: test-plan's count parser strips ANSI SGR sequences (a real runner interleaves them between tokens), and build-test's output trimming now rescues runner summary lines from the omitted middle (a failing suite's tail is all failure details, which pushed the one-line summary out of the kept text entirely).

Why it's needed

The existing pipeline is strong at reading code and verifying findings by tracing, but a hands-on maintainer verification routinely produces evidence the pipeline structurally cannot: it checks what the author claimed against what actually happens, measures before/after behavior instead of inferring it, and attributes test coverage to individual changes. This PR moves the deterministic halves of those three habits into subcommands (evidence owned by code, rulings owned by judgment — the same split the skill already uses everywhere), so every review gets them instead of only the PRs a maintainer hand-verifies.

Reviewer Test Plan

How to verify

  • Run cd packages/cli && npx vitest run src/commands/review src/commands/review.test.ts; expect all 42 files and 1161 tests to pass, including the new test-plan.test.ts and base-tree.test.ts suites.
  • Review the design rationale: packages/core/src/skills/bundled/review/DESIGN.md gains three sections (base-tree, Test Plan check, per-hunk probes) and SKILL.md gains the Step 6 Test Plan check, the Step 4 A/B capability paragraph, and the Step 8 provenance block.
  • Exercise test-plan against a real PR: from a checkout, qwen review fetch-pr 8176 QwenLM/qwen-code --remote origin --out /tmp/fetch.json, then qwen review test-plan --plan /tmp/fetch.json --pr 8176 --repo QwenLM/qwen-code --worktree .qwen/tmp/review-pr-8176; expect it to find ## Reviewer Test Plan, rule the four vitest paths and three npm commands reproduces, and leave the test-count claim unchecked (no build-test report supplied).
  • Exercise base-tree on the same fetch: qwen review base-tree --plan /tmp/fetch.json --worktree .qwen/tmp/review-pr-8176; expect available: true with the merge base checked out and built in .qwen/tmp/review-pr-8176-base. qwen review cleanup pr-8176 must report Removed base worktree.
  • Confirm the non-blocking contract: a contradicted Test Plan claim appears in the composed body as Test Plan (not a blocker): … and never changes the event; a missing or stale test-plan report changes nothing at all.

Evidence (Before & After)

Verified end-to-end by driving the built CLI from this branch (fresh worktree, npm ci + npm run build) through a live /review 8176 --effort medium in tmux with a real model:

  • The orchestrator ran qwen review test-plan unprompted at Step 6; the report found ## Reviewer Test Plan and ruled 9 claims (8 reproduces, 1 unchecked), and the saved report carried the new provenance block (head 4c77f80 + merge-base c3620bc7a + per-gate table).
  • The A/B harness reproduced the compiled-artifact table from feat(core): Normalize tool-call terminal telemetry #8176's manual verification byte-for-byte, with both trees built by the new subcommands (base-tree for the merge base, build-test --build-only for the head):
BASE (c3620bc7a)  soft error, no classification: error_type=undefined
                  success carrying a stale error: error="stale" error_type="execution_failed"
                  blank function_name: function_name=""
PR   (4c77f80)    soft error, no classification: error_type="unknown"
                  success carrying a stale error: error=undefined error_type=undefined
                  blank function_name: function_name="unknown_tool"
  • The live efficacy report carried the new hunks section with honest accounting (8 candidates: 6 selected, 2 over cap; all 6 skippedForBudget when the probe baseline was red — zero silent drops).

Tested on

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

Environment (optional)

Isolated git worktree of this branch, npm ci + npm run build, built TUI driven in tmux (node packages/cli/dist/index.js --yolo) with the model from ~/.qwen/settings.json; subcommands exercised via the built packages/cli/dist/index.js.

Risk & Scope

  • Main risk or tradeoff: test-plan extraction is heuristic; it is biased toward silence (prose is never extracted, count mismatches are differs, absolute paths produce no claim) so a wrong ruling costs a note, never a blocker. base-tree costs one extra build, which is why it is verifier-on-demand rather than part of every review.
  • Not validated / out of scope: the per-hunk and mutant probes both depend on the probe worktree being able to collect tests; in environments where the sibling-worktree vitest baseline is red they honestly report inconclusive/skipped (pre-existing limitation of the probe tree, observed unchanged on this branch and worth a separate issue). macOS/Windows not exercised locally.
  • Breaking changes / migration notes: none. Two new subcommands, one new optional --build-only flag, additive report fields (hunks, test-plan report); no public API changes.

Linked Issues

Workflow reference: the maintainer verification on #8176 whose evidence shapes these three capabilities.

中文说明

本 PR 做了什么

/review 增加三个此前只能靠"读代码"近似的验证能力,全部借鉴自人工 maintainer 验证的工作方式(即 #8176 验证评论 背后的流程):

  1. Test Plan 断言核对qwen review test-plan)。PR 描述中的 Test Plan 是作者亲手写下的一组可证伪断言,而此前流水线完全没有读它。新子命令抽取其中可判定的三类——路径、npm script、测试数——逐条对照被审代码树裁定:diff 里没有、树上也没有的路径,或没有任何 workspace manifest 定义的 script,判 contradicted;与本次运行套件数字不符的测试数只判 differs、绝不判矛盾,因为断言与观测可能针对不同的套件。裁定结果在所有 verdict(含 Approve)的评审正文中披露,但绝不阻断、绝不封顶——沿用 deferred-checker 先例。
  2. base-tree A/B harnessqwen review base-tree)。此前 mergeBaseSha 只用于一件事——确定 diff 范围——没有任何步骤构建过 PR 所改动的"之前"的代码,因此所有"此改动改变/保持了可观测行为 X"的论断都靠读 diff 得出。新子命令在 sibling worktree 中构建 merge base(复用 build-test 的最小构建集与 widening 循环,并新增 --build-only 模式),verifier 的 brief 现在对比较型断言提供该能力:同一输入在两棵树各跑一次,引用两侧输出。按 finding 而非按 review 付费;base 不可用(无 merge base、可能过期、构建失败)永远作为 harness 事实报告,绝不成为针对 PR 的 finding。
  3. test-efficacy 的 per-hunk 红/绿探针。整体回退探针是全有全无,语句删除 mutant 只覆盖狭窄的 safety-verb 集合,因此由条件/返回值修改构成的 diff——也就是大多数 diff——产生零个 mutant、没有任何按改动归因的信号。第三种探针逐个反向应用 hunk 并重跑受影响测试,把"套件仍然绿"归因到没有任何测试守护的那个具体改动。它继承 mutant 的纪律:编译失败判 inconclusive(绝不判 killed)、按内容恢复、cap 与预算跳过的数量必须上报。

另有两个来自实测的配套修复:test-plan 的计数解析先剥离 ANSI SGR 序列(真实 runner 会把颜色码嵌在 token 之间);build-test 的输出截断现在会从被省略的中段抢救 runner 汇总行(失败套件的尾部全是失败详情,会把那一行汇总整个挤出保留文本)。

为什么需要

现有流水线擅长读代码和以追踪方式验证 finding,但人工 maintainer 验证能例行产出流水线在结构上给不出的证据:对照作者的声明核对实际发生的事、实测前后行为而非推断、把测试覆盖归因到具体改动。本 PR 把这三种习惯中可确定性化的一半移入子命令(证据归代码、裁定归判断——skill 一贯的拆分方式),让每次评审都获得它们,而不是只有被 maintainer 人工验证的 PR 才有。

Reviewer 测试计划

验证方式

  • 运行 cd packages/cli && npx vitest run src/commands/review src/commands/review.test.ts;预期 42 个文件、1161 项测试全部通过,含新增的 test-plan.test.tsbase-tree.test.ts
  • 阅读设计依据:packages/core/src/skills/bundled/review/DESIGN.md 新增三节(base-tree、Test Plan 核对、per-hunk 探针);SKILL.md 新增 Step 6 的 Test Plan 核对、Step 4 的 A/B 能力段落、Step 8 的 provenance 块。
  • 用真实 PR 验证 test-plan:在 checkout 中执行 qwen review fetch-pr 8176 QwenLM/qwen-code --remote origin --out /tmp/fetch.json,然后 qwen review test-plan --plan /tmp/fetch.json --pr 8176 --repo QwenLM/qwen-code --worktree .qwen/tmp/review-pr-8176;预期找到 ## Reviewer Test Plan,四个 vitest 路径与三个 npm 命令判 reproduces,测试数断言判 unchecked(未提供 build-test 报告)。
  • 在同一次 fetch 上验证 base-treeqwen review base-tree --plan /tmp/fetch.json --worktree .qwen/tmp/review-pr-8176;预期 available: true,merge base 在 .qwen/tmp/review-pr-8176-base 完成 checkout 与构建。qwen review cleanup pr-8176 必须输出 Removed base worktree
  • 确认非阻断契约:被判 contradicted 的 Test Plan 断言以 Test Plan (not a blocker): … 形式出现在合成正文中、绝不改变 event;test-plan 报告缺失或过期时不产生任何影响。

前后对比证据

以本分支构建产物(全新 worktree、npm ci + npm run build)在 tmux 中用真实模型完整跑了一次 /review 8176 --effort medium

  • orchestrator 在 Step 6 自发运行了 qwen review test-plan;报告找到 ## Reviewer Test Plan 并裁定 9 条断言(8 条 reproduces、1 条 unchecked),存档报告携带新的 provenance 块(head 4c77f80 + merge-base c3620bc7a + 逐 gate 结果表)。
  • A/B harness 逐字节复现了 feat(core): Normalize tool-call terminal telemetry #8176 人工验证中的编译产物对照表,两棵树均由新子命令构建(merge base 用 base-tree,head 用 build-test --build-only):
BASE (c3620bc7a)  软错误、无分类:        error_type=undefined
                  成功事件携带残留 error: error="stale" error_type="execution_failed"
                  空 function_name:      function_name=""
PR   (4c77f80)    软错误、无分类:        error_type="unknown"
                  成功事件携带残留 error: error=undefined error_type=undefined
                  空 function_name:      function_name="unknown_tool"
  • 实测 efficacy 报告携带新的 hunks 段且账目诚实(8 个候选:6 选中、2 超 cap;探针基线为红时 6 个全部记 skippedForBudget——零静默丢弃)。

测试平台

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

环境(可选)

本分支的隔离 git worktree,npm ci + npm run build,在 tmux 中驱动构建产物 TUI(node packages/cli/dist/index.js --yolo),模型取自 ~/.qwen/settings.json;子命令均通过构建产物 packages/cli/dist/index.js 执行。

风险与范围

  • 主要风险或取舍:test-plan 的抽取是启发式的;它偏向沉默(散文从不抽取、计数不符只判 differs、绝对路径不产生断言),因此错误裁定的代价是一条备注,绝不是阻断。base-tree 多花一次构建,这正是它按 verifier 需求触发、而非每次评审必跑的原因。
  • 未验证 / 范围外:per-hunk 与 mutant 探针都依赖探针 worktree 能收集到测试;在 sibling-worktree 的 vitest 基线为红的环境中,它们诚实地报告 inconclusive/skipped(探针树的既有限制,本分支上观察到的行为不变,值得另开 issue)。macOS/Windows 未在本地验证。
  • 破坏性变更 / 迁移说明:无。两个新子命令、一个新的可选 --build-only 标志、增量式报告字段(hunks、test-plan 报告);无公开 API 变更。

关联 Issue

工作流参考:#8176 上的 maintainer 验证,其证据形态即本 PR 三个能力的来源。

wenshao added 2 commits July 31, 2026 17:02
… A/B, per-hunk probes

- qwen review test-plan: rule on the PR Test Plan's checkable claims (paths,
  npm scripts, test counts) against the reviewed tree; contradictions and
  differing counts are disclosed via compose-review, never capping.
- qwen review base-tree: build the merge base in a sibling worktree so the
  verifier can A/B a comparative claim instead of reading it; swept by cleanup.
- test-efficacy: third probe kind — reverse-apply one hunk at a time and
  re-run the affected tests, attributing a still-green suite to the specific
  change nothing gates; shares the mutants' budget window, runs last.
… summaries

Both measured on a live /review of #8176 with the built CLI:

- test-plan's observedTestCounts strips SGR sequences before matching; a
  color-enabled pipe interleaves them BETWEEN tokens, and the count claim
  fell to 'unchecked' with the summary right there in the report.
- build-test's trimOutput rescues runner summary lines from the omitted
  middle (like module-resolution errors): a failing suite's tail is all
  failure details and npm epilogue, which pushed the one-line summary out
  of the kept text entirely.
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 31, 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 Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR! Re-running on the current head (7c6b678), which carries ~10 review-feedback commits since the last triage pass.

Template looks good ✓ — all required sections present, bilingual body, and a thorough Reviewer Test Plan with before/after evidence.

Problem: this is a feature, not a bug fix, so the question is whether the capability is needed rather than whether a bug reproduces. The motivation is grounded in real review experience — the pipeline today only ever reads code, so the author's own Test Plan claims go unverified, comparative claims ("this preserves the existing output") are settled by reading the diff, and per-change test attribution stops at the all-or-nothing revert probe. These are genuine, observed gaps in the review process (the #8176 manual verification is cited as the shape being automated), not theoretical hardening.

Direction: aligned. This is internal review tooling squarely within the project's quality-process mission — it makes the deterministic halves of hands-on verification available to every review rather than only hand-verified PRs. No CHANGELOG signal is expected for internal review tooling, and the area is clearly relevant. The author is a packages/core code-owner and maintainer (admin), so the core-touch two-tier gate is exempt; I still flag the size below for transparency.

Size: ~1,952 production-logic lines (all under packages/cli/src/commands/review/), ~2,039 test lines, and 88 lines of skill documentation in packages/core/src/skills/bundled/review/ (DESIGN.md + SKILL.md). The production count is up from ~1,506 at the last pass — the growth is hardening, not scope: false-positive bars in test-plan, ReDoS-safe regexes on untrusted PR-body input, the base-tree concurrency lock, and ANSI stripping in build-test, all from iterative review of this branch. This clears the 1,000-line large-PR advisory — see the approach note.

Approach: the code is well-structured and the three capabilities share real infrastructure (the extracted lib/worktree.ts discard helper, the shared probe budget, testPlanGate mirroring scriptLintGate). The one genuine question remains scope: test-plan, base-tree, and the per-hunk probes are largely independent and could have landed as three smaller, easier-to-revert PRs. They share design philosophy and plumbing, and the integration is clean — a question for the author, not a blocker.

Risk: no elevated risk signals — none of the changed files match the revert-correlated high-risk paths.

Moving on to code review. 🔍

中文说明

感谢贡献!在当前 head(7c6b678)上重新运行——自上次 triage 以来已新增约 10 个评审反馈 commit。

模板完整 ✓ —— 必填段落齐全,中英双语正文,Reviewer Test Plan 详尽并附前后对比证据。

问题:这是一个 feature 而非 bug fix,因此问题在于"是否需要这个能力",而非"bug 是否复现"。动机来自真实的评审经验——当前流水线只读代码,因此作者自己写下的 Test Plan 断言无人核对;比较型断言("此改动保持了原有输出")靠读 diff 得出;按改动归因的测试覆盖止步于全有全无的回退探针。这些都是评审流程中真实、已观测到的缺口(以 #8176 的人工验证为被自动化的形态),不是理论性加固。

方向:对齐。这是内部评审工具,完全在项目的质量流程使命之内。作者是 packages/core 的 code-owner 与维护者(admin),因此核心改动的两级门禁豁免;但仍就规模做透明说明。

规模:约 1,952 行生产逻辑(全部位于 packages/cli/src/commands/review/),约 2,039 行测试,以及 88 行 skill 文档。生产行数较上次(约 1,506)有所增加——增量是加固而非扩范围:test-plan 中的误报屏障、针对不可信 PR 正文的 ReDoS 安全正则、base-tree 并发锁,以及 build-test 中的 ANSI 剥离,均来自对本分支的迭代评审。超过 1,000 行大 PR 建议线——见方案说明。

方案:代码结构良好,三个能力共享真实的基础设施。唯一真正的疑问仍是范围:三者大体独立,本可拆成三个更小、更易回退的 PR。但它们共享设计哲学与管线,集成也很干净——是提给作者的问题,不是阻断项。

风险:无升级风险信号——改动文件均未命中与回退相关的高风险路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Code review

Independent proposal first: to let /review verify claims hands-on, I would (a) deterministically extract the checkable assertions from the PR's own Test Plan and rule on each, disclosed-but-non-blocking; (b) for comparative claims, build the merge base in a sibling worktree on demand rather than per review; and (c) for per-change attribution, extend test-efficacy with per-hunk reverse-apply probes that share the existing mutant budget. The PR's approach matches this almost exactly, and it reuses what is already there — base-tree rides runBuildTest's minimal-build set and widening loop via a new --build-only flag, testPlanGate mirrors scriptLintGate, and the hunk probes inherit the mutants' discipline. I did not find a materially simpler path it missed; the only alternative is splitting into three PRs, which is a process preference, not a correctness issue.

No critical blockers — no correctness bugs, security holes, or regressions in the production code. Conventions are clean throughout (ESM, kebab-case, collocated tests, license headers, no bare any, comments that explain why). The reuse discipline is good: the discardWorktree two-step that previously lived inside test-efficacy is extracted to lib/worktree.ts and shared with base-tree, baseWorktreePath sits beside probeWorktreePath so the suffix cannot drift between creator and sweeper, and cleanup sweeps both the new base tree and its build lock.

The non-blocking contract is carefully held, which is the thing most likely to go wrong in a gate PR: testPlanGate never caps and never blocks (a Test Plan defect is not a code defect, and capping on a missing report would cap essentially every PR — the deferred-checker precedent); base-tree reports an unavailable base (no merge base, a stale one via baseFetchFailed, or a build failure) as a harness fact, never a finding against the PR; and the hunk probes keep the mutants' asymmetry — a patch that will not apply or a tree that will not compile is inconclusive, never killed, and a hunk whose own collocated test left the baseline is never survived. Restore is by content (recreating the parent dir) rather than a forward re-apply that could fail and poison later probes.

The hardening that landed since the last pass is the right kind, and most of it is on untrusted input: the bold-line and heading regexes in test-plan are rewritten to be ReDoS-safe (the old shape backed off quadratically on a line a PR body controls), path claims now require a file extension or ./ prefix so a slug like owner/repo is not filed as a missing file, cd-prefixed repro commands resolve paths against the cd dir (with a bail on chained cd and --root-style flags), and npmScriptOf is an allowlist after a denylist read every other npm builtin as a missing script. base-tree gained a real mkdirSync test-and-set lock (EEXIST-only busy, a 30-min corpse sweep) so concurrent verifier shards no longer sweep the tree out from under each other mid-build. build-test's summary rescue is now capped (RESCUE_MAX=40) so it cannot make the trim a no-op on a large log.

Two non-blocking nits, neither worth holding the PR:

  • Scope. Three capabilities plus two supporting fixes in ~1,952 production lines. They share real plumbing, so the coupling is genuine, but three smaller PRs would have been easier to review and revert.
  • test-plan.ts is 756 lines of heuristic extraction (paths, commands, counts). It will need upkeep as Test Plan prose varies. The mitigations are real — it is biased toward silence (prose is never extracted, count mismatches are differs not contradicted, absolute paths and .. escapes produce no contradiction) and it carries 884 lines of tests — so this is a maintenance watch-item, not a defect.
Files changed (13 of 21 shown; 8 test files omitted)
File What changed
packages/cli/src/commands/review.ts registers the two new subcommands (base-tree, test-plan)
packages/cli/src/commands/review/base-tree.ts new: build the merge base in a sibling worktree (lock, markers, build-only) so a comparative claim can be measured
packages/cli/src/commands/review/build-test.ts adds build-only mode; capped rescue of runner summary lines (ANSI-stripped) from the trimmed middle
packages/cli/src/commands/review/cleanup.ts sweeps the base worktree and its build lock at review end
packages/cli/src/commands/review/compose-review.ts testPlanGate discloses Test Plan rulings on every verdict, never caps or blocks
packages/cli/src/commands/review/agent-prompt.ts verifier brief describes the per-hunk probes
packages/cli/src/commands/review/lib/agent-briefs.ts brief gains the hunk-survived finding kind and the A/B capability paragraph
packages/cli/src/commands/review/lib/paths.ts baseWorktreePath helper beside probeWorktreePath
packages/cli/src/commands/review/lib/worktree.ts new: shared discardWorktree (symlink-safe) plus worktree-create failure detail
packages/cli/src/commands/review/test-efficacy.ts per-hunk reverse-apply probes sharing the mutant budget and window
packages/cli/src/commands/review/test-plan.ts new: extract and rule on Test Plan path, command, and count claims (ReDoS-safe, false-positive-biased)
packages/core/src/skills/bundled/review/DESIGN.md three new design sections (base-tree, Test Plan check, per-hunk probes)
packages/core/src/skills/bundled/review/SKILL.md Step 6 Test Plan check, Step 4 A/B paragraph, Step 8 provenance block
…and 8 test files collocated unit + integration coverage for all of the above (~2,039 test lines)

Testing

This is an unattended CI run, so I did not build or run the PR's code — the evidence below is the PR's own CI, read via the API. The load-bearing Linux unit suite is green on the reviewed commit 7c6b678, as are the other PR-CI checks; nothing is red. (The only in-flight item, review-pr, is this triage job's own orchestration, not PR CI.)

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
Post Coverage Comment (ubuntu-latest, 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
route ✅ success

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

The deterministic subcommand behaviour is what the unit suite substantiates, and it is green. The end-to-end orchestration — the live /review 8176 in tmux with a real model that the Evidence section describes — is the author's claim and is not independently re-run here. Sandboxed verification would settle it if a maintainer wants it: @qwen-code /tmux drives a real /review end-to-end and captures the terminal, and @qwen-code /verify gives an A/B load-bearing proof against the base build. The author has write access, so both are available directly. Given the unit coverage is the load-bearing evidence for deterministic CLI subcommands and is green, this is optional confirmation rather than a gap that blocks.

中文说明

代码审查

先给出独立方案:要让 /review 能动手验证断言,我会 (a) 确定性地抽取 PR 自己 Test Plan 中可判定的断言并逐条裁定,披露但不阻断;(b) 对比较型断言,按需(而非每次评审)在 sibling worktree 中构建 merge base;(c) 对按改动归因,扩展 test-efficacy,复用现有 mutant 预算做 per-hunk 反向应用探针。本 PR 的方案几乎与此完全一致,并且复用既有能力。我没有找到它遗漏的更简路径;唯一的替代方案是拆成三个 PR,那是流程偏好,不是正确性问题。

未发现关键阻断项——生产代码中没有正确性 bug、安全漏洞或回归。约定全程干净(ESM、kebab-case、同目录测试、license 头、无裸 any、注释解释"为什么")。复用纪律良好。

非阻断契约被谨慎地守住——这正是 gate 类 PR 最容易出错的地方:testPlanGate 绝封顶、绝不阻断;base-tree 把 base 不可用作为 harness 事实报告,绝不作为针对 PR 的 finding;hunk 探针保持 mutant 的非对称性——补丁无法应用或树无法编译判 inconclusive、绝不判 killed,且其自身同目录测试离开基线的 hunk 绝不判 survived。恢复按内容(并重建父目录)而非可能失败并污染后续探针的正向重新应用。

自上次以来落地的加固是正确的一类,且大多针对不可信输入:test-plan 中的粗体行与标题正则被重写为 ReDoS 安全;路径断言现在要求文件扩展名或 ./ 前缀,以免把 owner/repo 这类 slug 误判为缺失文件;带 cd 前缀的复现命令对 cd 目录解析路径(并对链式 cd--root 类标志退出);npmScriptOf 改为允许名单。base-tree 增加了真正的 mkdirSync test-and-set 锁(仅 EEXIST 判忙、30 分钟僵尸清扫),使并发的验证者分片不再在构建中途互相清扫。build-test 的摘要抢救现在设上限(RESCUE_MAX=40)。

两个非阻断的小意见,都不足以扣住这个 PR:

  • 范围。 三个能力加两个配套修复,约 1,952 行生产代码。共享管线是真的,但三个更小的 PR 会更容易审查与回退。
  • test-plan.ts 是 756 行启发式抽取。随着 Test Plan 措辞变化需要维护。缓解是真实的——它偏向沉默,并配有 884 行测试——所以是维护观察项,不是缺陷。

测试

这是无人值守的 CI 运行,因此我没有构建或运行 PR 的代码——以下证据是 PR 自己的 CI,经 API 读取。承载性的 Linux 单测套件在所审 commit 7c6b678 上为绿,其余 PR-CI 检查亦为绿,无任何变红。(唯一在跑的 review-pr 是本次 triage 任务自身的编排,不是 PR CI。)

确定性子命令的行为由单测套件证实,且为绿。端到端编排——Evidence 一节描述的、用真实模型在 tmux 中跑 /review 8176——是作者的声明,此处未独立复跑。若维护者需要,沙盒验证可以落定:@qwen-code /tmux 端到端驱动一次真实 /review 并捕获终端,@qwen-code /verify 给出对 base 构建的 A/B 承载性证明。作者有写权限,两者均可直接使用。鉴于单测覆盖是确定性 CLI 子命令的承载性证据且为绿,这属于可选确认,而非阻断性缺口。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean, well-reasoned code from a maintainer who owns this exact area, now green on the current head; the only reservations are non-blocking (scope, and the heuristic parser's upkeep).

Stepping back: this is a maintainer-authored PR (the author is a packages/core code-owner with admin), so the core-touch gate is exempt and the question is simply whether the code is good. It is. Going back to my independent proposal, the PR matches it almost line for line and reuses the existing machinery rather than building parallel paths — base-tree rides runBuildTest, testPlanGate mirrors scriptLintGate, the hunk probes inherit the mutants' discipline. I did not find a simpler path it missed.

What I am most satisfied with is the restraint. A gate PR's failure mode is over-blocking, and this one goes out of its way to never block and never cap on the wrong thing — a Test Plan defect is disclosed, not a verdict; an unavailable base is a harness fact, not a finding; a hunk that will not reverse-apply is inconclusive, never killed, and one whose own test left the baseline is never survived. That is exactly the "a wrong comment costs more than a missing one" philosophy the review skill is built on, applied consistently. The hardening that landed since the last pass (ReDoS-safe regexes on untrusted PR-body input, the base-tree concurrency lock, the false-positive bars) is the right kind, and it came from iterative review of this branch rather than expanding scope.

The reservations are honest ones, not blockers. Three capabilities in ~1,952 production lines is a lot to review and revert at once; I would have preferred three PRs, but the shared plumbing is real and the integration is clean. And test-plan.ts is a large heuristic surface that will need tending — though it is biased toward silence and carries 884 lines of tests, so it earns its place.

The Linux unit suite is green on this commit and the other PR-CI checks are clear, so the approval is no longer held. Approving now, pinned to the reviewed commit.

中文说明

置信度:4/5 —— 来自拥有该领域的维护者的干净、考虑周全的代码,如今在当前 head 上转绿;唯一的保留是非阻断性的(范围,以及启发式解析器的维护)。

退一步看:这是一个维护者署名的 PR(作者是拥有 admin 的 packages/core code-owner),因此核心改动门禁豁免,问题只是代码好不好。好。回到我的独立方案,本 PR 几乎逐行与之吻合,并且复用既有机制而非另起平行路径。我没有找到它遗漏的更简路径。

最让我满意的是克制。gate 类 PR 的失败模式是过度阻断,而本 PR 竭力做到绝不在错误的地方阻断或封顶——Test Plan 缺陷只披露、不裁定;base 不可用是 harness 事实、不是 finding;无法反向应用的 hunk 判 inconclusive、绝不判 killed,其自身测试离开基线的 hunk 绝不判 survived。这正是 review skill 赖以建立的"一条错误的评论比一条缺失的更昂贵"哲学,并被一以贯之地应用。自上次以来落地的加固(针对不可信 PR 正文的 ReDoS 安全正则、base-tree 并发锁、误报屏障)是正确的一类,且来自对本分支的迭代评审,而非扩大范围。

保留意见是诚实的,不是阻断项。三个能力放在约 1,952 行生产代码里,一次审查与回退的负担都不小;我本更希望拆成三个 PR,但共享管线是真的,集成也干净。而 test-plan.ts 是一大片需要打理的启发式面——不过它偏向沉默且配有 884 行测试,因此站得住脚。

Linux 单测套件在此 commit 上为绿,其余 PR-CI 检查亦清晰,因此审批不再暂缓。现在给出 approve,固定于所审 commit。

Qwen Code · qwen3.8-max-preview

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

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Summary

Package Lines Statements Functions Branches
CLI 83.33% 83.33% 89.45% 82.62%
Core 87.56% 87.56% 89.15% 86.2%
CLI Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |   83.33 |    82.62 |   89.45 |   83.33 |                   
 src               |    84.4 |    81.51 |   88.29 |    84.4 |                   
  cli.ts           |   96.03 |    84.61 |     100 |   96.03 | ...37-538,548-549 
  gemini.tsx       |   72.21 |    76.81 |   80.76 |   72.21 | ...1253-1257,1378 
  ...ractiveCli.ts |   86.46 |    82.39 |    87.5 |   86.46 | ...2492,2498,2553 
  ...liCommands.ts |   88.34 |     83.6 |      90 |   88.34 | ...63,480,514,635 
  ...ActiveAuth.ts |     100 |     87.5 |     100 |     100 | 66-80             
 ...cp-integration |   69.97 |    72.73 |   89.81 |   69.97 |                   
  acpAgent.ts      |   69.64 |     72.6 |   89.84 |   69.64 | ...65,11370-11372 
  authMethods.ts   |      92 |       60 |     100 |      92 | 33-34             
  errorCodes.ts    |       0 |        0 |       0 |       0 | 1-22              
  ...ion-skills.ts |     100 |    88.23 |     100 |     100 | 17,32             
  generation.ts    |    97.1 |    81.25 |     100 |    97.1 | 109,112           
  ...DirContext.ts |     100 |      100 |     100 |     100 |                   
 ...ration/service |   97.04 |    95.71 |   93.33 |   97.04 |                   
  filesystem.ts    |   97.04 |    95.71 |   93.33 |   97.04 | ...21-122,238-239 
 ...ration/session |   91.65 |    86.57 |   96.24 |   91.65 |                   
  Session.ts       |   91.26 |    85.45 |   95.67 |   91.26 | ...9407,9434-9438 
  ...entTracker.ts |   91.87 |    89.47 |      90 |   91.87 | ...43,207,286-295 
  ...stop-guard.ts |     100 |    98.07 |     100 |     100 | 37,127            
  ...eplay-page.ts |    93.1 |    90.72 |     100 |    93.1 | 71,82-85,111-121  
  ...y-replayer.ts |   98.53 |    95.52 |     100 |   98.53 | 238-240           
  index.ts         |       0 |        0 |       0 |       0 | 1-40              
  ...ssionUtils.ts |   89.76 |    87.32 |     100 |   89.76 | ...54-270,326-328 
  tasksSnapshot.ts |    94.3 |     87.5 |     100 |    94.3 | 65-71             
  ...on-tracker.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...ssion/emitters |   95.68 |     93.7 |   96.66 |   95.68 |                   
  ...ageEmitter.ts |   95.34 |    94.11 |     100 |   95.34 | 52-59             
  PlanEmitter.ts   |     100 |    83.33 |     100 |     100 | 59                
  base-emitter.ts  |   78.26 |       75 |     100 |   78.26 | 23-24,26-28       
  index.ts         |       0 |        0 |       0 |       0 | 1-10              
  ...ll-emitter.ts |   99.17 |    97.43 |     100 |   99.17 | 352-353           
 ...ession/rewrite |    91.8 |    89.13 |   94.44 |    91.8 |                   
  LlmRewriter.ts   |    82.4 |     86.2 |     100 |    82.4 | ...,88-89,166-170 
  ...Middleware.ts |   96.96 |    88.09 |     100 |   96.96 | 144,152-154       
  TurnBuffer.ts    |     100 |      100 |     100 |     100 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 src/commands      |    89.2 |    73.75 |   64.51 |    89.2 |                   
  auth.ts          |     100 |    83.33 |     100 |     100 | 11,14             
  channel.ts       |   55.55 |      100 |       0 |   55.55 | 18-22,30-40       
  extensions.tsx   |   96.77 |      100 |      50 |   96.77 | 39                
  hooks.tsx        |   66.66 |      100 |       0 |   66.66 | 20-24             
  mcp.ts           |   95.45 |      100 |      50 |   95.45 | 31                
  review.ts        |   98.18 |      100 |      50 |   98.18 | 66                
  serve.ts         |    87.1 |    68.22 |     100 |    87.1 | ...46-649,663-667 
  sessions.ts      |     100 |      100 |      50 |     100 |                   
  update.ts        |   98.13 |    94.44 |   66.66 |   98.13 | 82-83             
 ...mmands/channel |   88.29 |    87.64 |    90.3 |   88.29 |                   
  channel-cwd.ts   |     100 |      100 |     100 |     100 |                   
  ...l-registry.ts |      80 |    84.61 |      80 |      80 | 37-40,49-52,63-66 
  ...entry-path.ts |      75 |       50 |     100 |      75 | 8-9               
  config-utils.ts  |   95.87 |    96.35 |     100 |   95.87 | ...08-213,271-274 
  configure.ts     |    14.7 |      100 |       0 |    14.7 | 18-21,23-84       
  daemon-worker.ts |   93.89 |    85.33 |   94.11 |   93.89 | ...1209,1216-1217 
  loop-runtime.ts  |   91.66 |      100 |      50 |   91.66 | 15,22             
  ...classifier.ts |   98.49 |    96.51 |     100 |   98.49 | 115-116,161       
  ...tact-store.ts |   93.51 |    87.65 |     100 |   93.51 | ...71,288-289,337 
  pairing.ts       |   72.85 |      100 |      50 |   72.85 | 22-28,57-68       
  pidfile.ts       |   95.55 |       90 |     100 |   95.55 | ...50-251,315-316 
  proxy.ts         |     100 |      100 |     100 |     100 |                   
  reload.ts        |    77.5 |    86.95 |      75 |    77.5 | 72-84,93-97       
  runtime.ts       |   82.43 |    86.44 |     100 |   82.43 | ...87-191,251-253 
  set.ts           |   75.72 |    85.71 |      50 |   75.72 | 65-83,111-116     
  start.ts         |    85.8 |    82.17 |      88 |    85.8 | ...85,591-594,606 
  ...ure-format.ts |   93.65 |    82.45 |     100 |   93.65 | ...42,48-49,74-75 
  status.ts        |   78.57 |    59.25 |   66.66 |   78.57 | ...36-137,150-161 
  stop.ts          |   57.83 |    82.35 |      50 |   57.83 | ...3,74-76,85-111 
 ...nds/extensions |   88.82 |    87.64 |   87.09 |   88.82 |                   
  consent.ts       |   72.53 |       90 |   42.85 |   72.53 | ...86-142,157-163 
  disable.ts       |     100 |       90 |     100 |     100 | 30                
  enable.ts        |     100 |    91.66 |     100 |     100 | 38                
  install.ts       |   82.95 |    81.57 |      75 |   82.95 | ...96-199,202-211 
  link.ts          |     100 |      100 |     100 |     100 |                   
  list.ts          |     100 |     87.5 |     100 |     100 | 18                
  new.ts           |     100 |      100 |     100 |     100 |                   
  settings.ts      |   99.15 |      100 |   83.33 |   99.15 | 151               
  sources.ts       |   93.42 |    87.09 |   92.85 |   93.42 | ...4-66,96-98,167 
  uninstall.ts     |   74.57 |       40 |   66.66 |   74.57 | 45-47,60-67,70-73 
  update.ts        |   96.71 |    97.05 |     100 |   96.71 | 114-118           
  utils.ts         |      75 |    53.84 |     100 |      75 | ...27-131,133-137 
 ...les/mcp-server |       0 |        0 |       0 |       0 |                   
  example.ts       |       0 |        0 |       0 |       0 | 1-60              
 ...amples/starter |       0 |        0 |       0 |       0 |                   
  example.ts       |       0 |        0 |       0 |       0 | 1-64              
 src/commands/mcp  |   90.17 |    84.39 |   83.33 |   90.17 |                   
  add.ts           |    99.3 |    96.07 |     100 |    99.3 | 154-155           
  approve.ts       |   76.19 |     87.5 |   66.66 |   76.19 | ...,89-99,114-124 
  list.ts          |   92.59 |    83.87 |      80 |   92.59 | ...62-164,180-181 
  reconnect.ts     |   78.85 |    66.66 |   85.71 |   78.85 | 42-55,169-191     
  remove.ts        |     100 |       80 |     100 |     100 | 21-25             
 ...ommands/review |    85.3 |    86.34 |   86.26 |    85.3 |                   
  agent-prompt.ts  |   90.88 |    92.78 |      96 |   90.88 | ...1273,1743-1812 
  base-tree.ts     |   74.87 |    79.16 |   77.77 |   74.87 | ...29-350,352-365 
  capture-local.ts |   68.57 |     90.9 |      75 |   68.57 | 107-111,158-189   
  ...k-coverage.ts |   48.38 |    14.28 |   66.66 |   48.38 | ...21-226,239-249 
  cleanup.ts       |   89.12 |    82.22 |   83.33 |   89.12 | ...00-505,507-508 
  ...ent-status.ts |   93.03 |    83.87 |   83.33 |   93.03 | 291,531-551       
  ...ose-review.ts |   95.45 |    90.88 |   94.11 |   95.45 | ...1535,1563-1585 
  fetch-pr.ts      |   74.02 |    53.57 |      50 |   74.02 | ...98,332,402-407 
  load-rules.ts    |   26.41 |      100 |   16.66 |   26.41 | ...41-153,155-156 
  parse-args.ts    |   99.27 |       96 |     100 |   99.27 | 345,417           
  plan-diff.ts     |   64.04 |      100 |   66.66 |   64.04 | 127-163           
  pr-context.ts    |   84.44 |    79.38 |   91.66 |   84.44 | ...29-910,939-941 
  presubmit.ts     |   83.75 |    92.72 |   88.88 |   83.75 | ...77-578,655-685 
  ...ve-anchors.ts |   77.02 |    88.46 |      75 |   77.02 | ...70-175,187-204 
  run.ts           |   81.14 |    86.17 |    90.9 |   81.14 | ...13,429-477,490 
  script-lint.ts   |   83.67 |    78.41 |   88.88 |   83.67 | ...59-773,775-797 
  submit.ts        |   76.74 |    82.05 |   81.81 |   76.74 | ...03-639,641-642 
  test-efficacy.ts |   87.57 |    82.79 |   94.73 |   87.57 | ...1973,1981-2001 
  test-plan.ts     |      90 |    92.63 |   88.23 |      90 | ...73-674,738-755 
 ...nds/review/lib |   95.89 |    93.29 |   95.83 |   95.89 |                   
  agent-briefs.ts  |   98.68 |      100 |       0 |   98.68 | 537-538           
  anchors.ts       |     100 |    94.79 |     100 |     100 | ...33,169,178,225 
  coverage.ts      |   95.47 |    94.25 |   95.45 |   95.47 | ...98,335,433-450 
  diff-flags.ts    |     100 |        0 |     100 |     100 | 63                
  diff-plan.ts     |   98.73 |    93.01 |     100 |   98.73 | ...41,264,290-291 
  effort.ts        |     100 |      100 |     100 |     100 |                   
  gh.ts            |    85.6 |    88.37 |   71.42 |    85.6 | ...20,257-258,285 
  git.ts           |   97.64 |    95.65 |     100 |   97.64 | 180-181           
  heavy.ts         |     100 |      100 |     100 |     100 |                   
  inline-counts.ts |     100 |      100 |     100 |     100 |                   
  local-diff.ts    |    84.4 |    88.46 |     100 |    84.4 | ...63-473,475-483 
  merge-base.ts    |     100 |      100 |     100 |     100 |                   
  path-rules.ts    |     100 |      100 |     100 |     100 |                   
  paths.ts         |     100 |     87.5 |     100 |     100 | 84                
  prompt-record.ts |   94.73 |    88.23 |     100 |   94.73 | ...28,151-152,156 
  receipt.ts       |     100 |      100 |     100 |     100 |                   
  report.ts        |   94.38 |    93.75 |     100 |   94.38 | 173-177           
  roster.ts        |     100 |    94.23 |     100 |     100 | 143,161,206       
  shell-quote.ts   |     100 |      100 |     100 |     100 |                   
  test-utils.ts    |     100 |      100 |     100 |     100 |                   
  transcripts.ts   |   96.27 |    93.18 |     100 |   96.27 | ...83,269-270,294 
  workspaces.ts    |   98.88 |    92.39 |     100 |   98.88 | 212-213           
  worktree.ts      |     100 |      100 |     100 |     100 |                   
 ...mands/sessions |   91.56 |    86.95 |   83.33 |   91.56 |                   
  common.ts        |     100 |      100 |     100 |     100 |                   
  list.ts          |   90.96 |    86.66 |   81.81 |   90.96 | 208-219,221-222   
 src/config        |   94.38 |    88.91 |   95.95 |   94.38 |                   
  auth.ts          |   89.35 |    83.56 |     100 |   89.35 | ...97-298,314-315 
  ...eMcpImport.ts |   87.91 |    81.52 |     100 |   87.91 | ...63-371,453-454 
  compile-cache.ts |     100 |      100 |     100 |     100 |                   
  config.ts        |   89.14 |    88.67 |   86.11 |   89.14 | ...2436,2438-2446 
  ...cy-monitor.ts |   88.75 |    76.19 |     100 |   88.75 | ...3,90-92,98,101 
  ...ust-policy.ts |   83.04 |    88.28 |     100 |   83.04 | ...39,253,352-353 
  ...heme-names.ts |     100 |      100 |     100 |     100 |                   
  environment.ts   |   94.33 |    89.61 |   94.73 |   94.33 | ...35-639,655-656 
  ...le-watcher.ts |   90.86 |    83.65 |   95.83 |   90.86 | ...23-325,370,418 
  ...resh-state.ts |   90.57 |    97.29 |   93.75 |   90.57 | 137-142,146-152   
  ...ime-reload.ts |     100 |    69.69 |     100 |     100 | ...12-113,122-123 
  hot-reload.ts    |     100 |    89.13 |     100 |     100 | 47,172-178,238    
  keyBindings.ts   |   97.43 |       50 |     100 |   97.43 | 236-239           
  ...ngsAdapter.ts |     100 |    94.11 |     100 |     100 | 64                
  ...ig-watcher.ts |   95.17 |    83.05 |     100 |   95.17 | ...78,200,292-293 
  ...er-secrets.ts |   98.97 |    96.87 |     100 |   98.97 | 85                
  mcpApprovals.ts  |   96.55 |    95.55 |     100 |   96.55 | 223-224,229-231   
  mcpJson.ts       |     100 |      100 |     100 |     100 |                   
  mcpServers.ts    |   92.85 |     87.5 |     100 |   92.85 | 46-47             
  ...idersScope.ts |      95 |    94.73 |     100 |      95 | 11-12             
  ...abledTools.ts |     100 |      100 |     100 |     100 |                   
  ...comparison.ts |     100 |      100 |     100 |     100 |                   
  ...n-settings.ts |   99.15 |    93.75 |     100 |   99.15 | 63                
  sandboxConfig.ts |   61.64 |    71.87 |   66.66 |   61.64 | ...54-68,73,77-89 
  ...ings-cache.ts |   98.26 |    97.14 |     100 |   98.26 | 201-202           
  settings.ts      |   90.99 |     92.3 |      90 |   90.99 | ...1006,1008-1009 
  ...ingsSchema.ts |     100 |      100 |     100 |     100 |                   
  ...ngsWatcher.ts |   95.54 |    88.34 |     100 |   95.54 | ...28,277-278,293 
  ...d-env-keys.ts |     100 |      100 |     100 |     100 |                   
  ...l-settings.ts |     100 |      100 |     100 |     100 |                   
  ...paths-lite.ts |   89.47 |       88 |     100 |   89.47 | 43-44,53-54,56-57 
  ...tedFolders.ts |   93.35 |    94.11 |     100 |   93.35 | ...90-391,427-438 
 ...nfig/migration |   95.23 |    77.77 |   83.33 |   95.23 |                   
  index.ts         |   95.65 |     87.5 |     100 |   95.65 | 117-118           
  scheduler.ts     |   96.55 |    77.77 |     100 |   96.55 | 19-20             
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...ation/versions |   94.91 |      100 |     100 |   94.91 |                   
  ...-v2-shared.ts |     100 |      100 |     100 |     100 |                   
  v1-to-v2.ts      |   81.75 |      100 |     100 |   81.75 | ...28-229,231-247 
  v2-to-v3.ts      |     100 |      100 |     100 |     100 |                   
  v3-to-v4.ts      |     100 |      100 |     100 |     100 |                   
  v5-to-v4.ts      |      96 |      100 |     100 |      96 | 94-95,99          
 src/core          |     100 |      100 |     100 |     100 |                   
  auth.ts          |     100 |      100 |     100 |     100 |                   
  initializer.ts   |     100 |      100 |     100 |     100 |                   
  theme.ts         |     100 |      100 |     100 |     100 |                   
 src/dualOutput    |    71.8 |    70.31 |   66.66 |    71.8 |                   
  ...tputBridge.ts |   71.95 |    70.96 |   68.42 |   71.95 | ...08-409,417-420 
  ...utContext.tsx |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-8               
 src/export        |       0 |        0 |       0 |       0 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-7               
 src/generated     |     100 |      100 |     100 |     100 |                   
  git-commit.ts    |     100 |      100 |     100 |     100 |                   
 src/hooks         |     100 |      100 |     100 |     100 |                   
  ...elete-hook.ts |     100 |      100 |     100 |     100 |                   
 src/i18n          |   85.98 |    81.92 |   89.65 |   85.98 |                   
  index.ts         |   73.45 |    77.77 |      90 |   73.45 | ...70-271,294-299 
  languages.ts     |   93.07 |     92.3 |   85.71 |   93.07 | ...35,164-169,184 
  ...nslateKeys.ts |     100 |      100 |     100 |     100 |                   
  ...lationDict.ts |   93.33 |    66.66 |     100 |   93.33 | 15                
 src/i18n/locales  |     100 |      100 |     100 |     100 |                   
  ca.js            |     100 |      100 |     100 |     100 |                   
  de.js            |     100 |      100 |     100 |     100 |                   
  en.js            |     100 |      100 |     100 |     100 |                   
  fr.js            |     100 |      100 |     100 |     100 |                   
  ja.js            |     100 |      100 |     100 |     100 |                   
  pt.js            |     100 |      100 |     100 |     100 |                   
  ru.js            |     100 |      100 |     100 |     100 |                   
  zh-TW.js         |     100 |      100 |     100 |     100 |                   
  zh.js            |     100 |      100 |     100 |     100 |                   
 ...nonInteractive |      80 |    76.31 |   81.35 |      80 |                   
  session.ts       |   84.08 |    75.27 |   93.61 |   84.08 | ...1007,1016-1026 
  types.ts         |    42.5 |      100 |   33.33 |    42.5 | ...24-625,628-629 
 ...active/control |   75.63 |    89.09 |      80 |   75.63 |                   
  ...rolContext.ts |    6.45 |        0 |       0 |    6.45 | 56-95             
  ...Dispatcher.ts |   91.79 |    92.45 |   88.88 |   91.79 | ...49-367,387,390 
  ...rolService.ts |    6.89 |        0 |       0 |    6.89 | 46-188            
 ...ol/controllers |   42.27 |    64.66 |   48.64 |   42.27 |                   
  ...Controller.ts |   39.49 |      100 |      80 |   39.49 | 88-92,127-210     
  ...Controller.ts |       0 |        0 |       0 |       0 | 1-56              
  ...Controller.ts |   53.96 |    65.71 |   58.33 |   53.96 | ...37-642,644-649 
  ...Controller.ts |   14.06 |      100 |       0 |   14.06 | ...82-117,130-133 
  ...Controller.ts |   37.92 |    60.71 |   46.66 |   37.92 | ...41-653,662-691 
 .../control/types |       0 |        0 |       0 |       0 |                   
  serviceAPIs.ts   |       0 |        0 |       0 |       0 | 1                 
 ...Interactive/io |   98.07 |    94.05 |   95.23 |   98.07 |                   
  ...putAdapter.ts |   97.98 |     93.2 |   98.07 |   97.98 | ...1415,1431-1432 
  ...putAdapter.ts |      96 |    91.66 |   85.71 |      96 | 51-52             
  ...nputReader.ts |     100 |    94.73 |     100 |     100 | 67                
  ...putAdapter.ts |   98.38 |      100 |   90.47 |   98.38 | 84-85,125-126     
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/patches       |       0 |        0 |       0 |       0 |                   
  is-in-ci.ts      |       0 |        0 |       0 |       0 | 1-17              
 src/remoteInput   |   87.31 |    75.32 |   88.23 |   87.31 |                   
  ...utContext.tsx |     100 |      100 |     100 |     100 |                   
  ...putWatcher.ts |   88.01 |       76 |   93.33 |   88.01 | ...49-350,361-364 
  index.ts         |       0 |        0 |       0 |       0 | 1-8               
 src/serve         |   87.32 |    83.58 |    91.1 |   87.32 |                   
  ...tp-enabled.ts |     100 |      100 |     100 |     100 |                   
  ...ion-bridge.ts |     100 |      100 |     100 |     100 |                   
  auth.ts          |    93.4 |    92.95 |     100 |    93.4 | ...19-320,323-325 
  ...em-adapter.ts |     100 |      100 |     100 |     100 |                   
  capabilities.ts  |     100 |    97.95 |     100 |     100 | 649               
  ...cp-command.ts |     100 |      100 |     100 |     100 |                   
  ...horization.ts |   92.79 |    93.33 |    87.5 |   92.79 | 75-80,135-136     
  ...livery-ipc.ts |     100 |     90.9 |     100 |     100 | 94,106,134        
  ...l-delivery.ts |     100 |      100 |     100 |     100 |                   
  ...op-mcp-ipc.ts |   81.06 |    73.68 |   94.11 |   81.06 | ...37-242,267,289 
  ...nt-service.ts |   93.89 |    86.61 |     100 |   93.89 | ...66-468,475,477 
  ...-selection.ts |     100 |      100 |     100 |     100 |                   
  ...ings-store.ts |   85.85 |    91.78 |   95.83 |   85.85 | ...94-206,366-369 
  ...ebhook-ipc.ts |    98.5 |    86.66 |     100 |    98.5 | 47                
  ...iagnostics.ts |     100 |      100 |     100 |     100 |                   
  ...worker-env.ts |     100 |      100 |     100 |     100 |                   
  ...rker-group.ts |   87.27 |     85.2 |     100 |   87.27 | ...10,816-820,838 
  ...er-manager.ts |   89.39 |    83.88 |   93.33 |   89.39 | ...98,711,722-724 
  ...tartup-ipc.ts |   97.72 |    96.66 |     100 |   97.72 | 88-89             
  ...supervisor.ts |   92.41 |    84.44 |    97.1 |   92.41 | ...1460,1514-1518 
  ...e-grouping.ts |     100 |    94.11 |     100 |     100 | 69,132            
  core-runtime.ts  |     100 |      100 |     100 |     100 |                   
  ...ub-session.ts |   92.04 |    77.77 |     100 |   92.04 | ...36-445,470,508 
  daemon-logger.ts |    82.2 |    77.26 |   91.76 |    82.2 | ...1720,1747-1753 
  ...trics-ring.ts |     100 |      100 |     100 |     100 |                   
  ...s-provider.ts |   68.04 |    52.77 |     100 |   68.04 | ...44-249,282-290 
  daemon-status.ts |   98.37 |    90.06 |     100 |   98.37 | ...1041,1043-1044 
  debug-mode.ts    |     100 |      100 |     100 |     100 |                   
  demo.ts          |     100 |      100 |     100 |     100 |                   
  env-snapshot.ts  |   93.37 |    85.18 |     100 |   93.37 | 114-117,195-202   
  ...-scheduler.ts |   87.34 |    83.87 |     100 |   87.34 | 33-36,48-50,79-81 
  ...-path-argv.ts |     100 |      100 |     100 |     100 |                   
  ...h-settings.ts |   94.41 |    88.75 |     100 |   94.41 | ...24,702,718,728 
  fast-path.ts     |   90.49 |     80.7 |   95.45 |   90.49 | ...92-501,567-568 
  ...ration-sse.ts |   42.55 |    33.33 |     100 |   42.55 | 23-24,30,33-56    
  health-query.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-143             
  ...e-observer.ts |   89.89 |    83.24 |      96 |   89.89 | ...11-512,541-543 
  ...back-binds.ts |     100 |    88.88 |     100 |     100 | 32                
  ...-workspace.ts |    90.9 |    85.71 |     100 |    90.9 | ...27-128,139-140 
  ...iders-edit.ts |     100 |    82.14 |     100 |     100 | 58-60,65,81       
  ...ory-picker.ts |     100 |    86.95 |     100 |     100 | 36,66,92          
  ...sion-audit.ts |     100 |      100 |   93.33 |     100 |                   
  rate-limit.ts    |   92.77 |    88.42 |     100 |   92.77 | ...93-295,307-309 
  ...qwen-serve.ts |   82.69 |    79.33 |   74.06 |   82.69 | ...6744,6749-6750 
  ...tup-errors.ts |     100 |      100 |     100 |     100 |                   
  ...-keepalive.ts |   94.19 |     87.5 |     100 |   94.19 | ...26,530-531,571 
  ...-lifecycle.ts |     100 |      100 |     100 |     100 |                   
  server.ts        |   93.57 |    93.29 |   74.02 |   93.57 | ...2148,2169-2173 
  ...on-helpers.ts |     100 |      100 |     100 |     100 |                   
  ...t-event-id.ts |     100 |    95.23 |     100 |     100 | 12                
  ...-admission.ts |   98.71 |    89.65 |     100 |   98.71 | 68                
  types.ts         |     100 |      100 |     100 |     100 |                   
  ...erver-name.ts |     100 |      100 |     100 |     100 |                   
  ...ion-limits.ts |     100 |      100 |     100 |     100 |                   
  ...t-sessions.ts |    93.3 |    76.83 |     100 |    93.3 | ...13,816,829-831 
  ...l-resolver.ts |   90.32 |    66.66 |     100 |   90.32 | 16,45-46          
  ...ell-static.ts |   91.07 |    86.66 |     100 |   91.07 | ...79-182,216-219 
  ...ace-agents.ts |   66.13 |    70.57 |   92.68 |   66.13 | ...2246,2256-2266 
  ...generation.ts |    95.4 |    82.35 |   66.66 |    95.4 | 55-56,78,92       
  ...-git-state.ts |     100 |    91.93 |    90.9 |     100 | 161,172,202,265   
  ...ace-inputs.ts |     100 |      100 |     100 |     100 |                   
  ...-constants.ts |     100 |      100 |     100 |     100 |                   
  ...-summaries.ts |   86.66 |       50 |     100 |   86.66 | 11,19             
  ...ace-memory.ts |      83 |    74.54 |     100 |      83 | ...30-537,597-604 
  ...ers-status.ts |   98.58 |       79 |     100 |   98.58 | 106,134,174,177   
  ...tion-store.ts |   89.67 |    88.27 |   92.59 |   89.67 | ...91-400,411-414 
  ...e-registry.ts |   93.89 |     87.5 |     100 |   93.89 | ...18-519,525-526 
  ...ber-errors.ts |     100 |    95.32 |     100 |     100 | 53,93-94,172,192  
  ...e-remember.ts |   98.23 |    92.51 |     100 |   98.23 | ...36,340-345,386 
  ...te-runtime.ts |   83.98 |    90.29 |     100 |   83.98 | ...48-156,216-237 
  ...me-storage.ts |     100 |      100 |     100 |     100 |                   
  ...management.ts |   72.63 |    72.72 |      96 |   72.63 | ...88-889,896-900 
  ...ls-mapping.ts |     100 |      100 |     100 |     100 |                   
  ...lls-status.ts |     100 |    95.45 |     100 |     100 | 152               
  ...reconciler.ts |    91.6 |     83.9 |     100 |    91.6 | ...70-272,305-306 
 ...serve/acp-http |   77.04 |    78.33 |   93.26 |   77.04 |                   
  ...r-registry.ts |   96.92 |    94.87 |     100 |   96.92 | 184-187           
  client-mcp-ws.ts |   54.85 |    58.62 |   72.72 |   54.85 | ...99-300,304-305 
  ...n-registry.ts |    98.2 |    88.62 |     100 |    98.2 | 1015,1041-1052    
  dispatch.ts      |   71.44 |    73.89 |   95.34 |   71.44 | ...4733,4781-4787 
  index.ts         |   81.93 |    79.92 |    90.9 |   81.93 | ...2291,2375-2376 
  json-rpc.ts      |     100 |    96.96 |     100 |     100 | 92                
  safe-ws-send.ts  |   52.94 |    71.42 |     100 |   52.94 | 33-42,47-55       
  sse-stream.ts    |   93.96 |    88.57 |   84.61 |   93.96 | ...57-159,161-163 
  ...ort-stream.ts |       0 |        0 |       0 |       0 | 1                 
  ws-stream.ts     |   91.86 |       80 |     100 |   91.86 | 45,50,96,100-103  
 src/serve/auth    |   86.86 |     79.7 |   93.87 |   86.86 |                   
  device-flow.ts   |   96.35 |    80.57 |   97.61 |   96.35 | ...1358,1453,1519 
  ...w-provider.ts |   44.24 |    74.07 |   71.42 |   44.24 | ...23-284,297,301 
 ...rve/cdp-tunnel |   85.73 |    73.17 |    97.5 |   85.73 |                   
  ...r-emulator.ts |   88.57 |    63.63 |     100 |   88.57 | ...72-175,194-195 
  ...verse-link.ts |      88 |    76.19 |     100 |      88 | ...28-329,420-423 
  ...l-registry.ts |     100 |      100 |     100 |     100 |                   
  cdp-ws.ts        |   76.28 |    61.29 |    87.5 |   76.28 | ...13-217,223-228 
 ...nel/acceptance |       0 |        0 |       0 |       0 |                   
  ...mcp-smoke.mjs |       0 |        0 |       0 |       0 | 1-119             
  ...cceptance.mjs |       0 |        0 |       0 |       0 | 1-473             
  real-tab.mjs     |       0 |        0 |       0 |       0 | 1-218             
 src/serve/fs      |    86.4 |    80.74 |     100 |    86.4 |                   
  audit.ts         |     100 |    96.15 |     100 |     100 | 204               
  errors.ts        |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...x-registry.ts |     100 |      100 |     100 |     100 |                   
  paths.ts         |   77.64 |     73.8 |     100 |   77.64 | ...65,594-598,611 
  policy.ts        |   90.42 |    89.18 |     100 |   90.42 | 161-169           
  text-cursor.ts   |   88.23 |       90 |     100 |   88.23 | 74-77,92-95       
  ...ile-system.ts |   86.17 |    79.55 |     100 |   86.17 | ...2506,2516-2517 
 src/serve/routes  |   85.55 |    79.55 |   95.47 |   85.55 |                   
  a2ui-action.ts   |   96.84 |     88.5 |    87.5 |   96.84 | ...70-272,309-311 
  capabilities.ts  |     100 |      100 |     100 |     100 |                   
  ...nel-notify.ts |   85.22 |       88 |     100 |   85.22 | ...,83-87,103-104 
  ...l-webhooks.ts |   93.56 |    84.09 |     100 |   93.56 | ...42,292,332,334 
  daemon-status.ts |   85.45 |    83.33 |     100 |   85.45 | 98-105            
  goals.ts         |   98.92 |     90.9 |     100 |   98.92 | 146               
  health-demo.ts   |   94.73 |     86.2 |     100 |   94.73 | 62-66,154         
  permission.ts    |     100 |     92.3 |     100 |     100 | 50,98             
  ...uled-tasks.ts |   87.12 |    82.73 |   92.59 |   87.12 | ...1263,1306-1307 
  ...on-runtime.ts |     100 |    90.47 |     100 |     100 | 58,94             
  session.ts       |   85.21 |    81.81 |   95.16 |   85.21 | ...4608,4610-4611 
  sse-events.ts    |   84.45 |     87.5 |   77.77 |   84.45 | ...36,453-456,485 
  usage-stats.ts   |     100 |    95.45 |     100 |     100 | 118               
  ...space-auth.ts |   85.55 |    75.64 |     100 |   85.55 | ...21-326,331,345 
  ...el-control.ts |   86.26 |    78.94 |     100 |   86.26 | ...17-318,339-347 
  ...management.ts |   90.19 |    77.68 |     100 |   90.19 | ...47-448,467-468 
  ...d-contacts.ts |     100 |      100 |     100 |     100 |                   
  ...controller.ts |   83.09 |       79 |      90 |   83.09 | ...1032,1038,1041 
  ...extensions.ts |   87.23 |    72.76 |   94.11 |   87.23 | ...1826,1871-1872 
  ...-file-read.ts |      91 |    80.91 |     100 |      91 | ...20-621,624-625 
  ...file-write.ts |   84.44 |    64.51 |     100 |   84.44 | ...73-275,355-357 
  ...t-branches.ts |   75.43 |    66.66 |     100 |   75.43 | ...13-618,627-634 
  ...e-git-diff.ts |   97.32 |    90.56 |     100 |   97.32 | 161-162,189-191   
  ...ce-git-log.ts |     100 |    93.18 |     100 |     100 | 52,77,188         
  workspace-git.ts |   77.08 |    89.65 |     100 |   77.08 | 97-118            
  ...github-prs.ts |   88.26 |    63.46 |     100 |   88.26 | ...38-239,264-265 
  ...-lifecycle.ts |   95.23 |    75.75 |     100 |   95.23 | ...50-151,186-187 
  ...management.ts |   88.22 |    85.29 |     100 |   88.22 | ...1546,1566-1571 
  ...cp-control.ts |    73.2 |    67.54 |   85.71 |    73.2 | ...27-633,644-645 
  ...ace-models.ts |   95.53 |    89.74 |     100 |   95.53 | ...52-157,296-297 
  ...ermissions.ts |    77.9 |    72.41 |     100 |    77.9 | ...69-277,298-316 
  ...e-settings.ts |   74.81 |    70.31 |     100 |   74.81 | ...47-658,664-665 
  ...tup-github.ts |   77.97 |    70.58 |   84.21 |   77.97 | ...46-352,397-398 
  ...ace-skills.ts |   69.87 |    78.12 |     100 |   69.87 | ...59-284,290-324 
  ...ace-status.ts |   82.94 |     74.5 |     100 |   82.94 | ...84-486,490-491 
  ...pace-tools.ts |   75.94 |    69.69 |   66.66 |   75.94 | ...59-164,193-194 
  ...pace-trust.ts |   78.42 |    64.78 |      80 |   78.42 | ...31-336,344-345 
  ...pace-voice.ts |   91.33 |    80.92 |     100 |   91.33 | ...70-673,676-678 
 src/serve/server  |   90.69 |    89.18 |   96.55 |   90.69 |                   
  access-log.ts    |   98.68 |     97.1 |     100 |   98.68 | 115,186           
  ...er-helpers.ts |   63.82 |    77.96 |   81.81 |   63.82 | ...16,330,332-347 
  ...w-registry.ts |    98.8 |    81.81 |     100 |    98.8 | 107               
  ...r-handlers.ts |   97.29 |       75 |     100 |   97.29 | 17                
  ...r-response.ts |   85.66 |    76.83 |     100 |   85.66 | ...02,719,782-791 
  fs-factory.ts    |     100 |    92.59 |     100 |     100 | 34,42,103,159     
  ...branch-ops.ts |     100 |      100 |     100 |     100 |                   
  ...t-deadline.ts |     100 |      100 |     100 |     100 |                   
  ...iter-setup.ts |      65 |    73.33 |   33.33 |      65 | 30-35,38-43,47-48 
  ...st-helpers.ts |   95.11 |    95.19 |     100 |   95.11 | ...65-167,422-427 
  self-origin.ts   |   76.19 |       80 |     100 |   76.19 | 45-54             
  ...e-features.ts |   94.81 |    90.32 |     100 |   94.81 | 175-181           
  ...on-archive.ts |   89.55 |    87.78 |   97.14 |   89.55 | ...32-836,888-889 
  ...ion-export.ts |     100 |    94.44 |     100 |     100 | 64                
  session-list.ts  |   93.55 |    91.01 |     100 |   93.55 | ...79,681-687,827 
  telemetry.ts     |   99.01 |    97.45 |     100 |   99.01 | ...19,633,775-777 
 src/serve/voice   |   83.35 |    92.22 |   90.47 |   83.35 |                   
  ...ice-config.ts |   84.61 |       30 |     100 |   84.61 | 90-99,103-104     
  voice-ws.ts      |   77.16 |    94.73 |   83.33 |   77.16 | ...68,483,521-523 
  ...oordinator.ts |     100 |    98.21 |     100 |     100 | 176               
 ...kspace-service |   89.11 |    86.15 |   90.69 |   89.11 |                   
  index.ts         |   88.66 |    85.77 |   89.47 |   88.66 | ...1286-1290,1293 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/services      |   92.25 |    88.51 |   97.82 |   92.25 |                   
  ...mandLoader.ts |     100 |    88.88 |     100 |     100 | 104-117           
  ...killLoader.ts |   97.14 |    87.87 |     100 |   97.14 | 140,151-152       
  ...andService.ts |   98.73 |      100 |     100 |   98.73 | 107               
  ...mandLoader.ts |   86.83 |    83.87 |     100 |   86.83 | ...30-335,340-345 
  ...omptLoader.ts |   79.55 |    88.29 |   83.33 |   79.55 | ...48,178,245-246 
  ...mandLoader.ts |   97.36 |    92.68 |     100 |   97.36 | 153,160-161       
  ...nd-factory.ts |   91.42 |    91.66 |     100 |   91.42 | 128,137-144       
  ...ation-tool.ts |     100 |    95.45 |     100 |     100 | 125               
  ...ndMetadata.ts |   98.23 |    96.72 |     100 |   98.23 | 83,87             
  commandUtils.ts  |      96 |     90.9 |     100 |      96 | 48                
  ...and-parser.ts |   90.69 |    85.71 |     100 |   90.69 | 63-66             
  ...ionService.ts |     100 |      100 |     100 |     100 |                   
  prompt-stash.ts  |   96.66 |    92.85 |     100 |   96.66 | 34-35             
  ...tree-lease.ts |   88.29 |    86.48 |     100 |   88.29 | ...91-196,229-230 
  ...low-loader.ts |     100 |    96.15 |     100 |     100 | 88                
  setup-github.ts  |    90.8 |    80.95 |     100 |    90.8 | ...49-450,457-458 
  ...-args-file.ts |   93.93 |    91.66 |    87.5 |   93.93 | 208-210,224-230   
  types.ts         |     100 |      100 |     100 |     100 |                   
  ...e-keyterms.ts |   98.64 |    95.71 |     100 |   98.64 | 116,142-143       
  voice-model.ts   |     100 |      100 |     100 |     100 |                   
  voice-service.ts |   90.37 |    87.87 |     100 |   90.37 | ...80,287,352-357 
  ...e-settings.ts |     100 |    95.23 |     100 |     100 | 19                
  ...ranscriber.ts |   90.46 |    82.19 |      96 |   90.46 | ...66-668,671-673 
 ...rvices/insight |     100 |      100 |     100 |     100 |                   
  dates.ts         |     100 |      100 |     100 |     100 |                   
 ...ght/generators |   88.91 |    86.29 |   96.15 |   88.91 |                   
  DataProcessor.ts |   88.28 |    86.24 |   94.73 |   88.28 | ...1352,1356-1363 
  ...tGenerator.ts |   98.24 |    85.71 |     100 |   98.24 | 47                
  ...teRenderer.ts |     100 |      100 |     100 |     100 |                   
 .../insight/types |       0 |       50 |      50 |       0 |                   
  ...sightTypes.ts |       0 |        0 |       0 |       0 |                   
  ...sightTypes.ts |       0 |        0 |       0 |       0 | 1                 
 ...mpt-processors |   97.27 |    94.04 |     100 |   97.27 |                   
  ...tProcessor.ts |     100 |      100 |     100 |     100 |                   
  ...eProcessor.ts |   94.52 |    84.21 |     100 |   94.52 | 46-47,93-94       
  ...tionParser.ts |     100 |      100 |     100 |     100 |                   
  ...lProcessor.ts |   97.41 |    95.65 |     100 |   97.41 | 95-98             
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/services/tips |   97.27 |    84.61 |     100 |   97.27 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  tipHistory.ts    |   92.59 |       70 |     100 |   92.59 | ...24,146,153,162 
  tipRegistry.ts   |     100 |      100 |     100 |     100 |                   
  tipScheduler.ts  |     100 |    91.66 |     100 |     100 | 55                
 src/startup       |   88.99 |    83.47 |    90.9 |   88.99 |                   
  ...p-prefetch.ts |   98.09 |    94.23 |    87.5 |   98.09 | 50,209,225-226    
  ...reeStartup.ts |   80.53 |     74.6 |     100 |   80.53 | ...94,403,409-412 
 src/test-utils    |   93.26 |       75 |   83.33 |   93.26 |                   
  ci-env.ts        |      88 |     62.5 |     100 |      88 | 22-23,28          
  ...omMatchers.ts |   69.69 |       50 |      50 |   69.69 | 32-35,37-39,45-47 
  ...andContext.ts |     100 |      100 |     100 |     100 |                   
  render.tsx       |     100 |      100 |     100 |     100 |                   
 src/ui            |   71.95 |    74.96 |   64.63 |   71.95 |                   
  App.tsx          |   33.33 |       75 |   33.33 |   33.33 | 32-86             
  AppContainer.tsx |   73.02 |    71.51 |   67.85 |   73.02 | ...3950,3991-3998 
  ...tionNudge.tsx |    9.58 |      100 |       0 |    9.58 | 24-94             
  ...ackDialog.tsx |    30.3 |      100 |       0 |    30.3 | 26-76             
  ...tionNudge.tsx |    7.69 |      100 |       0 |    7.69 | 25-103            
  colors.ts        |      60 |      100 |   35.29 |      60 | ...52,54-55,60-61 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  keyMatchers.ts   |   95.91 |    97.14 |     100 |   95.91 | 25-26             
  ...tic-colors.ts |     100 |      100 |     100 |     100 |                   
  ...ractiveUI.tsx |   68.12 |    64.86 |   33.33 |   68.12 | ...98,321,341-346 
  ...inePresets.ts |   96.27 |    83.87 |     100 |   96.27 | ...97,402,410-412 
  textConstants.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/ui/auth       |   58.53 |    66.18 |   51.06 |   58.53 |                   
  AuthDialog.tsx   |   59.01 |     42.1 |   16.66 |   59.01 | ...25,332-354,358 
  ...nProgress.tsx |       0 |        0 |       0 |       0 | 1-64              
  ...etupSteps.tsx |   60.21 |    70.73 |   57.69 |   60.21 | ...90,794,803,806 
  useAuth.ts       |    94.6 |    73.52 |     100 |    94.6 | ...21-222,241-247 
  ...rSetupFlow.ts |   43.18 |    33.33 |      50 |   43.18 | ...78-399,416-459 
 src/ui/commands   |   81.41 |    82.94 |    89.4 |   81.41 |                   
  aboutCommand.ts  |     100 |      100 |     100 |     100 |                   
  agentsCommand.ts |   83.78 |      100 |      60 |   83.78 | 30-32,42-44       
  ...odeCommand.ts |    93.1 |    95.23 |     100 |    93.1 | 77-82             
  arenaCommand.ts  |   63.89 |    65.71 |   65.21 |   63.89 | ...01-606,691-699 
  authCommand.ts   |     100 |      100 |     100 |     100 |                   
  branchCommand.ts |     100 |      100 |     100 |     100 |                   
  btwCommand.ts    |   94.32 |    77.41 |     100 |   94.32 | 35-36,114-119     
  bugCommand.ts    |     100 |    77.77 |     100 |     100 | 27,61             
  cdCommand.ts     |    92.3 |    82.75 |     100 |    92.3 | ...,94-99,178,187 
  clearCommand.ts  |    80.9 |    70.83 |     100 |    80.9 | ...24-125,133-142 
  ...essCommand.ts |   67.95 |    55.88 |      75 |   67.95 | ...86-187,201-204 
  ...astCommand.ts |   84.17 |       75 |     100 |   84.17 | ...,91-97,125-130 
  ...ig-command.ts |   93.12 |    88.42 |     100 |   93.12 | ...07-315,321-323 
  ...extCommand.ts |   68.28 |    70.14 |   84.61 |   68.28 | ...66-599,610-611 
  copyCommand.ts   |    98.7 |    96.29 |     100 |    98.7 | 66-67,172,272,323 
  deleteCommand.ts |     100 |      100 |     100 |     100 |                   
  diffCommand.ts   |     100 |    87.87 |     100 |     100 | ...63,231-232,245 
  ...ryCommand.tsx |   81.64 |    87.67 |    90.9 |   81.64 | ...73-278,325-332 
  docsCommand.ts   |     100 |     90.9 |     100 |     100 | 25                
  doctorCommand.ts |   65.37 |    81.88 |   94.11 |   65.37 | ...85-535,538-672 
  dreamCommand.ts  |   85.45 |    88.88 |     100 |   85.45 | 58-65             
  editorCommand.ts |     100 |      100 |     100 |     100 |                   
  ...rt-command.ts |   82.97 |    78.57 |     100 |   82.97 | 47-52,67-70,91-96 
  exportCommand.ts |   98.25 |    91.02 |     100 |   98.25 | ...81,198-199,364 
  ...onsCommand.ts |   52.31 |    56.25 |   69.23 |   52.31 | ...09,277-329,390 
  forgetCommand.ts |     100 |       90 |     100 |     100 | 59                
  forkCommand.ts   |     100 |    94.11 |     100 |     100 | 96,147            
  goalCommand.ts   |   91.13 |    83.72 |      90 |   91.13 | ...81-184,196-199 
  helpCommand.ts   |     100 |      100 |     100 |     100 |                   
  ...oryCommand.ts |     100 |      100 |     100 |     100 |                   
  hooksCommand.ts  |   81.13 |    65.71 |   85.71 |   81.13 | ...,86-93,131-132 
  ideCommand.ts    |   60.75 |    64.28 |   41.17 |   60.75 | ...05-306,310-324 
  ...figCommand.ts |   52.83 |    81.25 |      70 |   52.83 | ...74-319,321-330 
  initCommand.ts   |   91.86 |       80 |     100 |   91.86 | 48,83-88          
  ...ghtCommand.ts |   77.87 |    71.42 |     100 |   77.87 | ...44-245,250-272 
  ...ageCommand.ts |   93.45 |    89.06 |     100 |   93.45 | ...68-169,196-206 
  learn-command.ts |     100 |      100 |     100 |     100 |                   
  lspCommand.ts    |     100 |    86.95 |     100 |     100 | 31,101-102        
  mcpCommand.ts    |     100 |      100 |     100 |     100 |                   
  memoryCommand.ts |     100 |      100 |     100 |     100 |                   
  modelCommand.ts  |   86.27 |    83.01 |     100 |   86.27 | ...22-935,969-974 
  ...onsCommand.ts |     100 |      100 |     100 |     100 |                   
  planCommand.ts   |   78.82 |    76.92 |     100 |   78.82 | 30-35,51-56,68-73 
  quitCommand.ts   |     100 |      100 |     100 |     100 |                   
  recapCommand.ts  |   21.81 |      100 |      50 |   21.81 | 24-73             
  ...ns-command.ts |   98.83 |    81.81 |     100 |   98.83 | 100               
  ...berCommand.ts |     100 |     87.5 |     100 |     100 | 46                
  renameCommand.ts |   89.06 |    88.37 |     100 |   89.06 | ...72-176,202-209 
  ...oreCommand.ts |    90.9 |    86.04 |     100 |    90.9 | ...41-146,176-177 
  resumeCommand.ts |     100 |      100 |     100 |     100 |                   
  rewindCommand.ts |   81.25 |      100 |      50 |   81.25 | 20-22             
  ...ngsCommand.ts |     100 |      100 |     100 |     100 |                   
  ...hubCommand.ts |   89.47 |       75 |      80 |   89.47 | 54-59             
  skillsCommand.ts |   78.82 |    81.81 |     100 |   78.82 | 37-52,78,97       
  statsCommand.ts  |   90.65 |    76.73 |     100 |   90.65 | ...30-733,825-832 
  ...ineCommand.ts |     100 |      100 |     100 |     100 |                   
  ...aryCommand.ts |    6.43 |      100 |      50 |    6.43 | 31-330            
  tasksCommand.ts  |   77.22 |    72.13 |     100 |   77.22 | ...46-150,172-177 
  ...tupCommand.ts |     100 |      100 |     100 |     100 |                   
  themeCommand.ts  |     100 |      100 |     100 |     100 |                   
  toolsCommand.ts  |     100 |      100 |     100 |     100 |                   
  trustCommand.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
  ...te-command.ts |     100 |    94.11 |     100 |     100 | 74,148            
  vimCommand.ts    |   54.54 |      100 |      50 |   54.54 | 19-29             
  voice-command.ts |   93.57 |       88 |     100 |   93.57 | 35,97-102         
  ...owsCommand.ts |   91.86 |    78.87 |   66.66 |   91.86 | ...60-161,170-175 
 src/ui/components |   70.87 |    78.44 |   78.57 |   70.87 |                   
  AboutBox.tsx     |     100 |      100 |     100 |     100 |                   
  AnsiOutput.tsx   |   65.57 |      100 |      50 |   65.57 | 69-90             
  ApiKeyInput.tsx  |       0 |        0 |       0 |       0 | 1-97              
  AppHeader.tsx    |    88.7 |       75 |     100 |    88.7 | 36,38-43,45       
  ...odeDialog.tsx |   87.24 |    72.22 |   33.33 |   87.24 | ...85,233-238,245 
  AsciiArt.ts      |     100 |      100 |     100 |     100 |                   
  ...Indicator.tsx |   95.65 |    66.66 |     100 |   95.65 | 27,52             
  ...TextInput.tsx |   88.65 |    90.41 |     100 |   88.65 | ...84-286,300-302 
  Composer.tsx     |   94.49 |    66.66 |     100 |   94.49 | ...-72,84,139,153 
  ...entPrompt.tsx |     100 |      100 |     100 |     100 |                   
  ...ryDisplay.tsx |   75.89 |    62.06 |     100 |   75.89 | ...,88,93-108,113 
  ...geDisplay.tsx |   68.42 |    57.14 |     100 |   68.42 | 16-17,31-32,42-50 
  CronPill.tsx     |     100 |    93.75 |     100 |     100 | 19                
  ...ification.tsx |      84 |       60 |     100 |      84 | 23-24,40-42       
  ...gProfiler.tsx |       0 |        0 |       0 |       0 | 1-36              
  ...ogManager.tsx |       0 |        0 |       0 |       0 | 1-597             
  DiffDialog.tsx   |    53.5 |     37.5 |   69.23 |    53.5 | ...32-737,747-760 
  ...ngsDialog.tsx |       0 |        0 |       0 |       0 | 1-195             
  EffortDialog.tsx |   97.36 |      100 |     100 |   97.36 | 55-56             
  ExitWarning.tsx  |     100 |      100 |     100 |     100 |                   
  ...hProgress.tsx |    87.8 |    33.33 |     100 |    87.8 | 28-31,56          
  ...ustDialog.tsx |     100 |      100 |     100 |     100 |                   
  Footer.tsx       |   75.11 |     61.4 |      50 |   75.11 | ...48-253,271-275 
  ...ngSpinner.tsx |   68.42 |    85.71 |      50 |   68.42 | 35-52,73,80-81    
  GoalPill.tsx     |   83.33 |    76.92 |     100 |   83.33 | 24-30             
  Header.tsx       |   98.65 |    94.73 |     100 |   98.65 | 173,175           
  Help.tsx         |   98.33 |       90 |     100 |   98.33 | ...25,382,448-449 
  ...emDisplay.tsx |   78.51 |     65.3 |     100 |   78.51 | ...99,502,505-511 
  ...ngeDialog.tsx |     100 |      100 |     100 |     100 |                   
  InputPrompt.tsx  |   83.08 |    81.73 |      80 |   83.08 | ...2197,2223,2297 
  ...Shortcuts.tsx |     100 |       88 |     100 |     100 | 98,119            
  ...Indicator.tsx |   98.18 |    97.82 |     100 |   98.18 | 161-162           
  ...firmation.tsx |   91.42 |      100 |      50 |   91.42 | 26-31             
  MainContent.tsx  |    95.9 |    92.53 |      50 |    95.9 | ...99,445-449,452 
  MemoryDialog.tsx |   86.59 |    80.15 |     100 |   86.59 | ...34-435,485,553 
  ...geDisplay.tsx |       0 |        0 |       0 |       0 | 1-41              
  ModelDialog.tsx  |   83.41 |    71.65 |     100 |   83.41 | ...69,971,976-992 
  ...tsDisplay.tsx |     100 |    97.22 |     100 |     100 | 270               
  ...fications.tsx |       0 |        0 |       0 |       0 | 1-56              
  ...onsDialog.tsx |       0 |        0 |       0 |       0 | 1-1004            
  ...ryDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...icePrompt.tsx |   92.64 |    85.71 |     100 |   92.64 | 102-106,134-139   
  PrepareLabel.tsx |   91.66 |    77.27 |     100 |   91.66 | 73-75,77-79,110   
  ...atePrompt.tsx |       0 |        0 |       0 |       0 | 1-134             
  ...geDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...ngDisplay.tsx |       0 |        0 |       0 |       0 | 1-39              
  ...hProgress.tsx |   85.25 |    88.46 |     100 |   85.25 | 121-147           
  ...dSelector.tsx |   92.79 |    82.65 |     100 |   92.79 | ...19-323,354-370 
  ...ionPicker.tsx |   83.66 |    72.13 |     100 |   83.66 | ...96,402,444-466 
  ...onPreview.tsx |   93.58 |    83.78 |     100 |   93.58 | ...,70-71,195-197 
  ...ryDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...putPrompt.tsx |   72.56 |       80 |      40 |   72.56 | ...06-109,114-117 
  ...tedDialog.tsx |     100 |      100 |     100 |     100 |                   
  ...ngsDialog.tsx |   71.59 |    73.72 |   69.23 |   71.59 | ...1244,1250-1251 
  ...ionDialog.tsx |    92.3 |    96.15 |   33.33 |    92.3 | 60-63,68-75,164   
  ...putPrompt.tsx |    15.9 |      100 |       0 |    15.9 | 20-63             
  ...Indicator.tsx |   57.14 |      100 |       0 |   57.14 | 12-15             
  ...MoreLines.tsx |       0 |        0 |       0 |       0 | 1-40              
  ...iewDialog.tsx |   97.77 |    87.67 |     100 |   97.77 | ...97,305-307,324 
  ...tsDisplay.tsx |   95.86 |       75 |     100 |   95.86 | 67-71             
  ...ionPicker.tsx |       0 |        0 |       0 |       0 | 1-172             
  ...tivityTab.tsx |    3.94 |      100 |       0 |    3.94 | 27-275            
  StatsDialog.tsx  |    8.64 |      100 |       0 |    8.64 | ...76-111,130-322 
  StatsDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...ciencyTab.tsx |    78.9 |    56.52 |     100 |    78.9 | ...26,213,262-288 
  ...atmapView.tsx |    8.98 |      100 |       0 |    8.98 | 20-107            
  ...essionTab.tsx |      80 |    66.66 |     100 |      80 | ...70-277,283-300 
  ...ineDialog.tsx |    93.5 |    85.18 |     100 |    93.5 | ...05,267,287-289 
  ...yTodoList.tsx |   96.36 |    88.23 |     100 |   96.36 | 138-141           
  ...nsDisplay.tsx |   92.97 |    83.87 |     100 |   92.97 | ...45,248,275-277 
  ThemeDialog.tsx  |   89.95 |    46.15 |      75 |   89.95 | ...71-173,243-245 
  Tips.tsx         |   93.54 |       75 |     100 |   93.54 | 39-40             
  TodoDisplay.tsx  |     100 |      100 |     100 |     100 |                   
  ...tsDisplay.tsx |     100 |     87.5 |     100 |     100 | 31-32             
  TrustDialog.tsx  |     100 |    83.33 |     100 |     100 | 72-87             
  ...ification.tsx |   36.36 |      100 |       0 |   36.36 | 15-22             
  ...Indicator.tsx |    92.5 |     87.5 |     100 |    92.5 | 50-53             
  ...ackDialog.tsx |       0 |        0 |       0 |       0 | 1-134             
  ...xitDialog.tsx |   80.36 |    43.47 |      60 |   80.36 | ...24-238,248-251 
  ...odeVisuals.ts |   97.22 |    85.71 |     100 |   97.22 | 25                
  ...s-helpers.tsx |   66.25 |    81.25 |      50 |   66.25 | 25-32,46-53,62-72 
 ...nts/agent-view |   55.05 |    69.09 |      50 |   55.05 |                   
  ...atContent.tsx |    9.09 |      100 |       0 |    9.09 | 54-275,281-283    
  ...tChatView.tsx |   21.05 |      100 |       0 |   21.05 | 21-39             
  ...tComposer.tsx |   69.48 |    33.33 |   66.66 |   69.48 | ...51,269,277-279 
  AgentFooter.tsx  |   15.38 |      100 |       0 |   15.38 | 28-65             
  AgentHeader.tsx  |   15.38 |      100 |       0 |   15.38 | 27-64             
  AgentTabBar.tsx  |    87.9 |    63.88 |     100 |    87.9 | ...88,110-118,136 
  ...oryAdapter.ts |     100 |    91.83 |     100 |     100 | 103,109-110,138   
  index.ts         |       0 |        0 |       0 |       0 | 1-12              
 ...mponents/arena |    42.3 |    68.69 |   73.68 |    42.3 |                   
  ArenaCards.tsx   |   73.06 |    71.79 |   85.71 |   73.06 | ...83-185,321-326 
  ...ectDialog.tsx |   83.48 |    69.86 |   88.88 |   83.48 | ...88-392,409-410 
  ...artDialog.tsx |       0 |        0 |       0 |       0 | 1-166             
  ...tusDialog.tsx |       0 |        0 |       0 |       0 | 1-288             
  ...topDialog.tsx |       0 |        0 |       0 |       0 | 1-213             
 ...ackground-view |   82.95 |    81.71 |   92.72 |   82.95 |                   
  ...sksDialog.tsx |   78.73 |    77.65 |   84.61 |   78.73 | ...1809,1833-1839 
  ...TasksPill.tsx |   67.74 |    86.66 |     100 |   67.74 | ...04-124,132-140 
  ...gentPanel.tsx |   97.08 |    86.31 |     100 |   97.08 | 132,442-446,520   
  agent-forest.ts  |    99.2 |    93.93 |     100 |    99.2 | 258               
  ...Visibility.ts |     100 |      100 |     100 |     100 |                   
  ...e-overlay.tsx |    88.2 |    76.47 |     100 |    88.2 | ...36-138,140-142 
 ...nts/extensions |   84.32 |    76.78 |   83.33 |   84.32 |                   
  ...gerDialog.tsx |   82.15 |    76.08 |     100 |   82.15 | ...91-198,258,260 
  TabBar.tsx       |   97.29 |    88.88 |     100 |   97.29 | 33                
  index.ts         |       0 |        0 |       0 |       0 | 1-12              
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...tensions/steps |   46.26 |       85 |   58.82 |   46.26 |                   
  ...ctionStep.tsx |   95.12 |    92.85 |   85.71 |   95.12 | 84-86,89          
  ...etailStep.tsx |       0 |        0 |       0 |       0 | 1-145             
  ...nListStep.tsx |   75.26 |    88.37 |   66.66 |   75.26 | ...53,174,203-209 
  ...electStep.tsx |       0 |        0 |       0 |       0 | 1-83              
  ...nfirmStep.tsx |   16.32 |      100 |       0 |   16.32 | 28-74             
  index.ts         |       0 |        0 |       0 |       0 | 1-11              
 ...xtensions/tabs |   71.92 |    68.21 |   70.83 |   71.92 |                   
  DiscoverTab.tsx  |   68.22 |    67.66 |   55.55 |   68.22 | ...93,656-660,664 
  InstalledTab.tsx |   75.49 |    67.44 |   83.33 |   75.49 | ...77,782-783,820 
  SourcesTab.tsx   |   71.67 |    70.47 |   77.77 |   71.67 | ...28,547,621-633 
 ...tensions/views |   50.97 |    52.38 |   20.83 |   50.97 |                   
  ...tionsView.tsx |   73.75 |    56.36 |   66.66 |   73.75 | ...30,353,369-374 
  ...tionsView.tsx |   43.45 |    44.82 |    6.66 |   43.45 | ...98-405,408-420 
  ...etailView.tsx |    9.56 |      100 |       0 |    9.56 | 40-67,70-158      
 ...mponents/hooks |   87.11 |    81.37 |   91.89 |   87.11 |                   
  ...rListBody.tsx |   95.29 |    85.18 |     100 |   95.29 | 95-98             
  ...etailStep.tsx |   75.32 |    71.42 |      60 |   75.32 | ...56-169,173-186 
  ...etailStep.tsx |     100 |      100 |     100 |     100 |                   
  ...rListStep.tsx |     100 |      100 |     100 |     100 |                   
  ...entHeader.tsx |     100 |    85.71 |     100 |     100 | 47                
  ...rListStep.tsx |     100 |      100 |     100 |     100 |                   
  ...etailStep.tsx |     100 |      100 |     100 |     100 |                   
  ...abledStep.tsx |     100 |      100 |     100 |     100 |                   
  ...sListStep.tsx |     100 |      100 |     100 |     100 |                   
  ...entDialog.tsx |   72.29 |    70.49 |     100 |   72.29 | ...51,563-568,572 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-13              
  ...erGrouping.ts |     100 |      100 |     100 |     100 |                   
  sourceLabels.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...components/mcp |   40.04 |    61.53 |   70.58 |   40.04 |                   
  ...ealthPill.tsx |   68.42 |    85.71 |     100 |   68.42 | 40-46             
  ...entDialog.tsx |   32.09 |    26.19 |      40 |   32.09 | ...12,914,927-933 
  ...valDialog.tsx |   15.06 |      100 |       0 |   15.06 | 40-109            
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-35              
  types.ts         |     100 |      100 |     100 |     100 |                   
  utils.ts         |      97 |       95 |     100 |      97 | 24,113-114        
 ...ents/mcp/steps |   53.94 |    73.51 |   57.14 |   53.94 |                   
  ...icateStep.tsx |    5.65 |      100 |       0 |    5.65 | 40-66,69-308      
  ...electStep.tsx |   10.95 |      100 |       0 |   10.95 | 16-88             
  ...etailStep.tsx |     100 |      100 |     100 |     100 |                   
  ...eListStep.tsx |   99.09 |    97.36 |     100 |   99.09 | 71                
  ...etailStep.tsx |   62.83 |       60 |   33.33 |   62.83 | ...87-296,307-332 
  ...rListStep.tsx |   88.53 |    81.25 |     100 |   88.53 | ...64,170,175-180 
  ...etailStep.tsx |    10.3 |      100 |       0 |    10.3 | ...1,67-79,82-140 
  ToolListStep.tsx |   69.29 |       50 |     100 |   69.29 | ...23,126,135-144 
 ...nents/messages |   89.97 |    86.43 |   85.14 |   89.97 |                   
  ...ionDialog.tsx |   89.23 |     84.9 |   81.81 |   89.23 | ...75,593,611-613 
  BtwMessage.tsx   |     100 |      100 |     100 |     100 |                   
  ...upDisplay.tsx |     100 |    94.73 |     100 |     100 | ...43,289,402,432 
  ...onMessage.tsx |   92.06 |    82.35 |     100 |   92.06 | 58-60,62,64       
  ...nMessages.tsx |   93.46 |      100 |   76.92 |   93.46 | ...90-292,295-298 
  DiffRenderer.tsx |   93.17 |    86.02 |     100 |   93.17 | ...07,235-236,302 
  ...tsDisplay.tsx |   97.08 |    77.77 |     100 |   97.08 | 95,97,106         
  ...usMessage.tsx |   76.52 |     42.1 |   66.66 |   76.52 | ...00,102,125,156 
  ...tsDisplay.tsx |   95.52 |    88.31 |     100 |   95.52 | ...40,142,175-180 
  ...ssMessage.tsx |    12.5 |      100 |       0 |    12.5 | 18-59             
  ...edMessage.tsx |   21.05 |      100 |       0 |   21.05 | 23-39             
  ...sMessages.tsx |   59.04 |       50 |    37.5 |   59.04 | ...21-126,147-159 
  ...ryMessage.tsx |   13.63 |      100 |       0 |   13.63 | 23-64             
  ...onMessage.tsx |   91.38 |    81.02 |     100 |   91.38 | ...33-635,642-644 
  ...upMessage.tsx |   98.32 |    95.16 |     100 |   98.32 | 184-187,414       
  ToolMessage.tsx  |   92.49 |       85 |   93.33 |   92.49 | ...61-966,993-995 
 ...ponents/shared |   85.79 |    81.92 |   94.11 |   85.79 |                   
  ...ctionList.tsx |     100 |      100 |      75 |     100 |                   
  ...tonSelect.tsx |     100 |      100 |     100 |     100 |                   
  EnumSelector.tsx |     100 |    96.42 |     100 |     100 | 58                
  ...rBoundary.tsx |     100 |      100 |     100 |     100 |                   
  MaxSizedBox.tsx  |   84.71 |    86.86 |      90 |   84.71 | ...67-568,685-686 
  MultiSelect.tsx  |   93.58 |       75 |     100 |   93.58 | ...43,199-201,211 
  ...tonSelect.tsx |     100 |      100 |     100 |     100 |                   
  ...ontroller.tsx |     100 |    83.33 |     100 |     100 | 73,93-95          
  ...eSelector.tsx |     100 |       60 |     100 |     100 | 40-45             
  ...lableList.tsx |   81.48 |    84.84 |     100 |   81.48 | 46-66,73-76       
  StaticRender.tsx |   72.72 |      100 |     100 |   72.72 | 31-33             
  TextInput.tsx    |    80.8 |    67.24 |      80 |    80.8 | ...36-240,252-258 
  ...ontroller.tsx |     100 |    81.81 |     100 |     100 | 59-62             
  ...apsedTime.tsx |     100 |      100 |     100 |     100 |                   
  ...Indicator.tsx |     100 |      100 |     100 |     100 |                   
  ...lizedList.tsx |   88.51 |    83.75 |   81.81 |   88.51 | ...51-779,792,887 
  text-buffer.ts   |   85.98 |    81.81 |   97.91 |   85.98 | ...2664,2762-2763 
  ...er-actions.ts |   73.93 |    67.22 |     100 |   73.93 | ...32-733,934-936 
 ...ponents/skills |       0 |        0 |       0 |       0 |                   
  ...gerDialog.tsx |       0 |        0 |       0 |       0 | 1-681             
 ...ents/subagents |       0 |        0 |       0 |       0 |                   
  constants.ts     |       0 |        0 |       0 |       0 | 1-71              
  index.ts         |       0 |        0 |       0 |       0 | 1-11              
  reducers.tsx     |       0 |        0 |       0 |       0 | 1-190             
  types.ts         |       0 |        0 |       0 |       0 | 1-125             
  utils.ts         |       0 |        0 |       0 |       0 | 1-102             
 ...bagents/create |       0 |        0 |       0 |       0 |                   
  ...ionWizard.tsx |       0 |        0 |       0 |       0 | 1-299             
  ...rSelector.tsx |       0 |        0 |       0 |       0 | 1-85              
  ...onSummary.tsx |       0 |        0 |       0 |       0 | 1-331             
  ...tionInput.tsx |       0 |        0 |       0 |       0 | 1-177             
  ...dSelector.tsx |       0 |        0 |       0 |       0 | 1-63              
  ...nSelector.tsx |       0 |        0 |       0 |       0 | 1-58              
  ...EntryStep.tsx |       0 |        0 |       0 |       0 | 1-78              
  ToolSelector.tsx |       0 |        0 |       0 |       0 | 1-253             
 ...bagents/manage |   14.14 |    53.19 |    37.5 |   14.14 |                   
  ...ctionStep.tsx |       0 |        0 |       0 |       0 | 1-103             
  ...eleteStep.tsx |       0 |        0 |       0 |       0 | 1-62              
  ...tEditStep.tsx |       0 |        0 |       0 |       0 | 1-124             
  ...ctionStep.tsx |   35.61 |    59.52 |     100 |   35.61 | ...21-433,438-440 
  ...iewerStep.tsx |       0 |        0 |       0 |       0 | 1-73              
  ...gerDialog.tsx |       0 |        0 |       0 |       0 | 1-341             
 ...mponents/views |   69.81 |    72.64 |   61.11 |   69.81 |                   
  ContextUsage.tsx |   70.88 |    63.88 |      80 |   70.88 | ...20-426,463-557 
  DoctorReport.tsx |     9.8 |      100 |       0 |     9.8 | 25-54,57-131      
  ...sionsList.tsx |   88.05 |       75 |     100 |   88.05 | 70-77             
  McpStatus.tsx    |   92.01 |     73.8 |     100 |   92.01 | ...36,175-177,262 
  SkillsList.tsx   |   20.51 |      100 |       0 |   20.51 | 17-20,27-57       
  ToolsList.tsx    |     100 |      100 |     100 |     100 |                   
 src/ui/contexts   |   83.96 |    81.62 |    86.3 |   83.96 |                   
  ...ewContext.tsx |   64.83 |    88.88 |      50 |   64.83 | ...16-219,225-235 
  AppContext.tsx   |      80 |       50 |     100 |      80 | 19-20             
  ...ewContext.tsx |   92.45 |    62.79 |      50 |   92.45 | ...69-270,272-276 
  ...igContext.tsx |   81.81 |       50 |     100 |   81.81 | 15-16             
  ...ssContext.tsx |   85.65 |    84.85 |     100 |   85.65 | ...1612-1614,1620 
  ...owContext.tsx |   91.07 |    81.81 |     100 |   91.07 | 47-48,60-62       
  ...deContext.tsx |     100 |      100 |      50 |     100 |                   
  ...onContext.tsx |   80.77 |       80 |    92.3 |   80.77 | ...31-434,443-446 
  ...gsContext.tsx |     100 |      100 |     100 |     100 |                   
  ...usContext.tsx |     100 |      100 |     100 |     100 |                   
  ...ngContext.tsx |   71.42 |       50 |     100 |   71.42 | 17-20             
  ...utContext.tsx |   85.71 |      100 |   66.66 |   85.71 | 13-14             
  ...edContext.tsx |     100 |      100 |      50 |     100 |                   
  ...nsContext.tsx |   88.88 |       50 |     100 |   88.88 | 155-156           
  ...teContext.tsx |   86.66 |       50 |     100 |   86.66 | 234-235           
  ...deContext.tsx |      80 |     87.5 |      75 |      80 | ...11-112,118-120 
  ...rtContext.tsx |     100 |      100 |     100 |     100 |                   
 src/ui/daemon     |   88.35 |    73.51 |   95.45 |   88.35 |                   
  ...ui-adapter.ts |   88.35 |    73.51 |   95.45 |   88.35 | ...74,792-793,879 
 src/ui/editors    |       0 |        0 |       0 |       0 |                   
  ...ngsManager.ts |       0 |        0 |       0 |       0 | 1-67              
 src/ui/hooks      |   85.25 |    82.62 |   89.96 |   85.25 |                   
  ...dProcessor.ts |   85.53 |    85.13 |     100 |   85.53 | ...-970,1017-1018 
  ...ention-ref.ts |   97.72 |       84 |     100 |   97.72 | 65                
  keyToAnsi.ts     |    3.92 |      100 |       0 |    3.92 | 19-77             
  ...esourceRef.ts |     100 |      100 |     100 |     100 |                   
  ...completion.ts |     100 |    95.45 |     100 |     100 | 95                
  ...ention-ref.ts |     100 |      100 |     100 |     100 |                   
  ...dProcessor.ts |   94.62 |    73.58 |     100 |   94.62 | ...86-287,292-293 
  ...dProcessor.ts |   85.21 |     66.4 |   81.81 |   85.21 | ...1407,1428-1432 
  ...rt-command.ts |     100 |      100 |     100 |     100 |                   
  ...sced-flush.ts |     100 |      100 |     100 |     100 |                   
  ...oice-input.ts |   92.36 |    81.95 |   66.66 |   92.36 | ...00,502-503,658 
  ...ke-repaint.ts |     100 |      100 |     100 |     100 |                   
  ...amingState.ts |   12.22 |      100 |       0 |   12.22 | 54-157            
  ...agerDialog.ts |   88.23 |      100 |     100 |   88.23 | 20,24             
  ...dScrollbar.ts |     100 |      100 |     100 |     100 |                   
  ...ationFrame.ts |      52 |    63.63 |     100 |      52 | ...59,67-70,76-87 
  ...odeCommand.ts |   58.82 |      100 |     100 |   58.82 | 28,33-48          
  ...enaCommand.ts |      85 |      100 |     100 |      85 | 23-24,29          
  ...aInProcess.ts |   27.92 |       80 |      25 |   27.92 | ...69-170,173-175 
  ...Completion.ts |   86.44 |    88.48 |     100 |   86.44 | ...14-515,525-541 
  ...ifications.ts |   87.82 |    96.77 |     100 |   87.82 | 138-152           
  ...tIndicator.ts |   88.28 |    81.57 |     100 |   88.28 | ...66,175,179-187 
  ...waySummary.ts |   96.26 |       75 |     100 |   96.26 | 126-128,170       
  ...ndTaskView.ts |   94.81 |    76.59 |     100 |   94.81 | 162-166,255,261   
  ...chedScroll.ts |     100 |      100 |     100 |     100 |                   
  ...ketedPaste.ts |    23.8 |      100 |       0 |    23.8 | 19-37             
  ...nchCommand.ts |   94.85 |    80.76 |     100 |   94.85 | ...54,229,292-295 
  ...ompletion.tsx |   97.09 |    87.09 |     100 |   97.09 | ...23-324,334-335 
  ...dMigration.ts |    92.1 |    88.88 |     100 |    92.1 | 42-44             
  useCompletion.ts |   96.29 |    90.56 |     100 |   96.29 | ...17-218,222-223 
  ...nitMessage.ts |     100 |      100 |     100 |     100 |                   
  ...extualTips.ts |   78.26 |       50 |     100 |   78.26 | ...2,75-79,96-104 
  ...eteCommand.ts |   89.52 |    90.69 |     100 |   89.52 | ...98-106,114-115 
  ...ialogClose.ts |   36.11 |       10 |     100 |   36.11 | ...89-195,202-207 
  useDiffData.ts   |       0 |        0 |       0 |       0 | 1-87              
  ...oublePress.ts |   53.12 |       75 |     100 |   53.12 | 33-35,41-54       
  ...orSettings.ts |     100 |      100 |     100 |     100 |                   
  ...Completion.ts |   99.12 |    97.67 |     100 |   99.12 | 182-183           
  ...ionUpdates.ts |   93.72 |    92.98 |     100 |   93.72 | ...87-291,314-320 
  ...agerDialog.ts |   88.88 |      100 |     100 |   88.88 | 21,25             
  ...backDialog.ts |    63.9 |    76.47 |   66.66 |    63.9 | ...66-168,190-191 
  useFocus.ts      |     100 |      100 |     100 |     100 |                   
  ...olderTrust.ts |     100 |    93.33 |     100 |     100 | 62                
  ...ggestions.tsx |   96.47 |    78.94 |     100 |   96.47 | 121,155-156       
  ...miniStream.ts |   85.66 |    81.38 |   96.15 |   85.66 | ...3943,4111-4119 
  ...BranchName.ts |     100 |    94.44 |     100 |     100 | 54                
  ...oryManager.ts |   98.01 |    98.36 |     100 |   98.01 | 139-142           
  ...ooksDialog.ts |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...stListener.ts |     100 |      100 |     100 |     100 |                   
  ...nAuthError.ts |   76.19 |       50 |     100 |   76.19 | 39-40,43-45       
  ...putHistory.ts |   92.59 |    85.71 |     100 |   92.59 | 63-64,72,94-96    
  ...storyStore.ts |     100 |    94.11 |     100 |     100 | 69                
  useKeypress.ts   |     100 |      100 |     100 |     100 |                   
  ...rdProtocol.ts |   36.36 |      100 |       0 |   36.36 | 24-31             
  ...unchEditor.ts |   22.58 |      100 |      50 |   22.58 | 11-32,44-85       
  ...gIndicator.ts |     100 |    96.66 |     100 |     100 | 109               
  useLogger.ts     |      16 |      100 |       0 |      16 | 15-45             
  useMCPHealth.ts  |   63.15 |       80 |      50 |   63.15 | 42-52,64-67       
  ...cpApproval.ts |   93.12 |    86.11 |     100 |   93.12 | ...24-127,139-140 
  useMcpDialog.ts  |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...moryDialog.ts |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...oryMonitor.ts |   83.14 |    78.57 |     100 |   83.14 | 54-63,74-79       
  ...ssageQueue.ts |     100 |     97.5 |     100 |     100 | 99                
  ...delCommand.ts |     100 |       95 |     100 |     100 | 53                
  ...ouseEvents.ts |   94.56 |    94.87 |   83.33 |   94.56 | 77-81             
  ...raseCycler.ts |   84.74 |    76.47 |     100 |   84.74 | ...49,52-53,69-71 
  ...rredEditor.ts |   58.33 |    22.22 |     100 |   58.33 | 23-27,29-33       
  ...derUpdates.ts |    87.4 |    78.78 |     100 |    87.4 | ...71,321-333,381 
  useQwenAuth.ts   |     100 |      100 |     100 |     100 |                   
  ...lScheduler.ts |   88.95 |    86.95 |     100 |   88.95 | ...37-439,471-481 
  ...oryCommand.ts |       0 |        0 |       0 |       0 | 1-7               
  ...umeCommand.ts |   94.67 |    74.28 |     100 |   94.67 | ...19,174,233-238 
  ...ompletion.tsx |   90.67 |    83.33 |     100 |   90.67 | ...02,105,138-141 
  ...ectionList.ts |   97.12 |    96.22 |     100 |   97.12 | ...92-193,247-250 
  ...sionPicker.ts |   92.87 |    90.35 |     100 |   92.87 | ...99-501,503-505 
  ...earchInput.ts |     100 |    97.29 |     100 |     100 | 82                
  ...ngsCommand.ts |   18.75 |      100 |       0 |   18.75 | 10-25             
  ...ellHistory.ts |   93.28 |    80.95 |     100 |   93.28 | ...96,153-154,164 
  ...oryCommand.ts |   85.48 |    58.33 |     100 |   85.48 | 22-28,40,71       
  ...agerDialog.ts |   88.23 |      100 |     100 |   88.23 | 20,24             
  ...Completion.ts |   82.85 |    85.13 |   94.73 |   82.85 | ...78-680,688-724 
  ...tateAndRef.ts |     100 |      100 |     100 |     100 |                   
  ...tatsDialog.ts |     100 |      100 |     100 |     100 |                   
  useStatusLine.ts |   97.13 |    93.33 |     100 |   97.13 | ...78-382,478-485 
  ...eateDialog.ts |   88.23 |      100 |     100 |   88.23 | 14,18             
  ...mInProcess.ts |   27.35 |       80 |      25 |   27.35 | ...82-183,186-188 
  ...tification.ts |     100 |     87.5 |     100 |     100 | 50                
  ...alProgress.ts |   67.34 |    58.82 |   66.66 |   67.34 | 52-53,61-68,79-85 
  ...rminalSize.ts |     100 |      100 |     100 |     100 |                   
  ...emeCommand.ts |   67.01 |    29.41 |     100 |   67.01 | ...10-111,115-116 
  useTimer.ts      |   97.59 |    94.73 |     100 |   97.59 | 17-18             
  ...lMigration.ts |       0 |        0 |       0 |       0 |                   
  ...rustModify.ts |     100 |    90.47 |     100 |     100 | 112,134           
  useTurnDiffs.ts  |   95.12 |    78.57 |     100 |   95.12 | 133-134,156-157   
  ...elcomeBack.ts |   87.36 |     90.9 |     100 |   87.36 | ...,94-96,114-115 
  ...reeSession.ts |   93.75 |       70 |     100 |   93.75 | 47-48,72          
  vim.ts           |      74 |    67.56 |   69.23 |      74 | ...1854-1861,1869 
 src/ui/layouts    |    91.2 |    89.47 |     100 |    91.2 |                   
  ...AppLayout.tsx |    90.9 |     87.5 |     100 |    90.9 | 60-62,110-115,151 
  ...AppLayout.tsx |   91.66 |    92.85 |     100 |   91.66 | 75-80             
 src/ui/models     |   80.72 |       80 |   71.42 |   80.72 |                   
  ...ableModels.ts |   80.72 |       80 |   71.42 |   80.72 | ...,61-71,125-127 
 ...noninteractive |     100 |      100 |    6.66 |     100 |                   
  ...eractiveUi.ts |     100 |      100 |    6.66 |     100 |                   
 src/ui/selection  |   86.47 |    79.88 |   96.66 |   86.47 |                   
  screen-buffer.ts |   94.73 |    64.28 |     100 |   94.73 | 51-52             
  ...ion-coords.ts |     100 |      100 |     100 |     100 |                   
  ...ction-span.ts |   92.72 |       90 |     100 |   92.72 | 37-38,67-68       
  ...tion-state.ts |   85.71 |      100 |   88.88 |   85.71 | 51-58             
  ...ction-text.ts |   92.85 |    92.45 |     100 |   92.85 | 30-34,114-115     
  ...selection.tsx |   80.31 |    59.64 |     100 |   80.31 | ...13-314,330-331 
 src/ui/state      |      95 |    81.81 |     100 |      95 |                   
  extensions.ts    |      95 |    81.81 |     100 |      95 | 69-70,89          
 src/ui/themes     |    98.5 |    73.17 |     100 |    98.5 |                   
  ansi-light.ts    |     100 |      100 |     100 |     100 |                   
  ansi.ts          |     100 |      100 |     100 |     100 |                   
  atom-one-dark.ts |     100 |      100 |     100 |     100 |                   
  ayu-light.ts     |     100 |      100 |     100 |     100 |                   
  ayu.ts           |     100 |      100 |     100 |     100 |                   
  color-utils.ts   |   99.23 |    97.05 |     100 |   99.23 | 277-278           
  default-light.ts |     100 |      100 |     100 |     100 |                   
  default.ts       |     100 |      100 |     100 |     100 |                   
  ...inal-theme.ts |   88.59 |    85.96 |     100 |   88.59 | ...57-261,266-270 
  dracula.ts       |     100 |      100 |     100 |     100 |                   
  github-dark.ts   |     100 |      100 |     100 |     100 |                   
  github-light.ts  |     100 |      100 |     100 |     100 |                   
  googlecode.ts    |     100 |      100 |     100 |     100 |                   
  no-color.ts      |     100 |      100 |     100 |     100 |                   
  qwen-dark.ts     |     100 |      100 |     100 |     100 |                   
  qwen-light.ts    |     100 |      100 |     100 |     100 |                   
  ...tic-tokens.ts |     100 |      100 |     100 |     100 |                   
  ...-of-purple.ts |     100 |      100 |     100 |     100 |                   
  theme-manager.ts |   88.68 |    84.52 |     100 |   88.68 | ...83-392,397-398 
  theme.ts         |     100 |    38.02 |     100 |     100 | ...34-449,457-461 
  xcode.ts         |     100 |      100 |     100 |     100 |                   
 src/ui/utils      |   86.61 |    84.99 |   95.28 |   86.61 |                   
  ...Colorizer.tsx |   80.31 |    85.41 |     100 |   80.31 | ...00-201,313-339 
  ...nRenderer.tsx |   79.84 |     75.6 |     100 |   79.84 | ...66,270,328-329 
  ...wnDisplay.tsx |   92.87 |    93.46 |     100 |   92.87 | ...,955,1002-1020 
  ...idDiagram.tsx |   87.79 |    95.34 |     100 |   87.79 | 156-179           
  ...eRenderer.tsx |   92.38 |    81.91 |   95.23 |   92.38 | ...43-746,799-804 
  ...odeDisplay.ts |   94.28 |    85.71 |     100 |   94.28 | 23,40             
  asciiCharts.ts   |    96.7 |     87.5 |     100 |    96.7 | 170-177,278       
  ...dWorkUtils.ts |     100 |      100 |     100 |     100 |                   
  ...boardUtils.ts |   52.52 |    73.25 |   91.66 |   52.52 | ...23,626-635,638 
  commandUtils.ts  |   96.17 |    88.88 |     100 |   96.17 | ...77,179-180,323 
  computeStats.ts  |     100 |      100 |     100 |     100 |                   
  customBanner.ts  |   90.68 |    91.22 |     100 |   90.68 | ...13,324-327,334 
  displayUtils.ts  |   73.84 |    73.91 |     100 |   73.84 | ...34,36-40,42-46 
  formatters.ts    |   94.87 |    98.21 |     100 |   94.87 | 116-119           
  gradientUtils.ts |     100 |      100 |     100 |     100 |                   
  highlight.ts     |     100 |      100 |     100 |     100 |                   
  ...gap-notice.ts |     100 |      100 |     100 |     100 |                   
  ...oryMapping.ts |     100 |       95 |     100 |     100 | 44,103            
  historyUtils.ts  |      96 |    97.05 |     100 |      96 | 102-105           
  inline-math.ts   |   98.48 |    95.23 |     100 |   98.48 | 129-130           
  input-mouse.ts   |     100 |    85.71 |     100 |     100 | 48,93             
  isNarrowWidth.ts |     100 |      100 |     100 |     100 |                   
  ...olDetector.ts |   68.81 |       75 |   66.66 |   68.81 | ...27-132,160-161 
  latexRenderer.ts |   94.95 |     73.8 |     100 |   94.95 | ...76-178,184-187 
  layoutUtils.ts   |     100 |      100 |     100 |     100 |                   
  list-mouse.ts    |     100 |      100 |     100 |     100 |                   
  ...ightLoader.ts |     100 |       95 |     100 |     100 | 81                
  ...nUtilities.ts |   98.72 |    94.36 |     100 |   98.72 | 145-146           
  ...t-position.ts |     100 |     87.5 |     100 |     100 | 85                
  ...geRenderer.ts |   86.23 |    69.06 |   95.12 |   86.23 | ...1284,1324-1330 
  ...alRenderer.ts |   86.69 |     71.9 |     100 |   86.69 | ...1476,1513-1519 
  ...lsBySource.ts |     100 |    95.23 |     100 |     100 | 84                
  mouse.ts         |   92.85 |    74.19 |     100 |   92.85 | ...38,145,149-152 
  osc8.ts          |   90.43 |    78.33 |     100 |   90.43 | ...59,244,248-249 
  ...red-height.ts |   98.38 |     97.1 |     100 |   98.38 | 195-197           
  ...mConstants.ts |     100 |      100 |     100 |     100 |                   
  restoreGoal.ts   |     100 |      100 |     100 |     100 |                   
  ...storyUtils.ts |   79.32 |    77.37 |     100 |   79.32 | ...32-554,685-686 
  ...ickerUtils.ts |     100 |      100 |     100 |     100 |                   
  ...evel-label.ts |   77.77 |    66.66 |     100 |   77.77 | 18,22-24          
  ...are-cursor.ts |   89.47 |    85.71 |     100 |   89.47 | 39-44             
  ...ataService.ts |   93.17 |     79.1 |     100 |   93.17 | ...14,227,254-256 
  suggestions.ts   |     100 |      100 |     100 |     100 |                   
  ...izedOutput.ts |   94.94 |      100 |   88.88 |   94.94 | 112-117           
  ...nal-buffer.ts |     100 |      100 |     100 |     100 |                   
  ...wOptimizer.ts |     100 |    96.77 |     100 |     100 | 69                
  terminalSetup.ts |    4.37 |      100 |       0 |    4.37 | 44-393            
  textUtils.ts     |   97.94 |    95.45 |   94.11 |   97.94 | ...82-283,443-444 
  ...background.ts |     100 |      100 |     100 |     100 |                   
  todoSnapshot.ts  |   90.42 |    92.85 |     100 |   90.42 | ...06-207,240-241 
  ...isplay-map.ts |     100 |      100 |     100 |     100 |                   
  updateCheck.ts   |     100 |    92.75 |     100 |     100 | 227-239,331       
  ...ow-keyword.ts |     100 |      100 |     100 |     100 |                   
 ...i/utils/export |   75.03 |    59.89 |   94.59 |   75.03 |                   
  collect.ts       |   71.27 |    65.81 |      96 |   71.27 | ...90-633,655-656 
  index.ts         |     100 |      100 |     100 |     100 |                   
  normalize.ts     |   80.42 |    50.68 |     100 |   80.42 | ...59-364,376-378 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
  utils.ts         |     100 |      100 |     100 |     100 |                   
 ...ort/formatters |   52.92 |    47.22 |   71.42 |   52.92 |                   
  html.ts          |   84.61 |       50 |     100 |   84.61 | ...53,57-58,62-63 
  json.ts          |     100 |      100 |     100 |     100 |                   
  jsonl.ts         |   82.45 |     37.5 |     100 |   82.45 | ...48,50-51,65-66 
  markdown.ts      |   36.32 |    47.05 |      50 |   36.32 | ...16-219,233-295 
 src/ui/voice      |   80.94 |    72.69 |   80.55 |   80.94 |                   
  ...d-recorder.ts |     6.2 |      100 |       0 |     6.2 | ...33-159,162-163 
  ...o-recorder.ts |   84.61 |    93.33 |   57.14 |   84.61 | ...16-117,131-136 
  ...me-session.ts |   89.72 |    65.33 |   93.75 |   89.72 | ...99,305,316-319 
  sox-recorder.ts  |    92.7 |    71.87 |     100 |    92.7 | ...34-135,153-154 
  ...ailability.ts |     100 |      100 |     100 |     100 |                   
  ...e-keyterms.ts |     100 |      100 |     100 |     100 |                   
  voice-model.ts   |     100 |      100 |     100 |     100 |                   
  ...e-recorder.ts |   88.29 |    67.74 |   81.81 |   88.29 | ...,98-99,112,115 
  voice-refine.ts  |     100 |    93.33 |     100 |     100 | 92                
  ...ream-retry.ts |   86.79 |    68.42 |     100 |   86.79 | 16-18,48-49,59-60 
  ...am-session.ts |   88.02 |    66.66 |   84.61 |   88.02 | ...26,343-345,363 
  ...ranscriber.ts |     100 |      100 |     100 |     100 |                   
 src/utils         |   81.37 |    86.94 |   92.54 |   81.37 |                   
  ...p-profiler.ts |   98.39 |    90.56 |     100 |   98.39 | 141,185,235       
  acpModelUtils.ts |   97.36 |    95.19 |     100 |   97.36 | ...09-210,214-215 
  apiPreconnect.ts |   96.74 |    94.59 |     100 |   96.74 | 167-170           
  ...ol-call-id.ts |   84.61 |       60 |     100 |   84.61 | 26-27,37-38       
  ...ng-failure.ts |     100 |       95 |     100 |     100 | 72                
  checks.ts        |   33.33 |      100 |       0 |   33.33 | 23-28             
  ...-api-error.ts |     100 |    96.42 |     100 |     100 | 14                
  cleanup.ts       |   84.05 |    94.11 |      80 |   84.05 | 80,111-121        
  commands.ts      |   97.45 |    96.66 |     100 |   97.45 | 153-155           
  ...Calculator.ts |     100 |      100 |     100 |     100 |                   
  cpuProfiler.ts   |   70.73 |    73.23 |   88.88 |   70.73 | ...27,430-431,438 
  deepMerge.ts     |     100 |       90 |     100 |     100 | 41-43,49          
  ...re-runtime.ts |     100 |      100 |     100 |     100 |                   
  ...ScopeUtils.ts |   97.56 |    88.88 |     100 |   97.56 | 67                
  doctorChecks.ts  |   70.31 |    74.57 |     100 |   70.31 | ...95-301,325-341 
  ...putCapture.ts |   90.65 |    86.31 |     100 |   90.65 | ...73,371,373-374 
  ...arResolver.ts |   97.14 |    96.55 |     100 |   97.14 | 125-126           
  errors.ts        |   97.56 |    94.64 |     100 |   97.56 | 69-70,304-305     
  events.ts        |     100 |      100 |     100 |     100 |                   
  ...on-mention.ts |   88.48 |     82.6 |     100 |   88.48 | ...56-160,164-168 
  gitUtils.ts      |   92.85 |    86.66 |     100 |   92.85 | ...13-116,164-167 
  ...AutoUpdate.ts |    93.1 |       94 |      90 |    93.1 | 103,108,179-190   
  ...tyWarnings.ts |     100 |      100 |     100 |     100 |                   
  ...lationInfo.ts |   97.68 |    94.28 |     100 |   97.68 | ...64,381-382,427 
  jsonc-editor.ts  |   93.18 |    92.72 |     100 |   93.18 | ...80-381,384-385 
  languageUtils.ts |   98.88 |    97.05 |     100 |   98.88 | 184-185           
  load-undici.ts   |     100 |      100 |     100 |     100 |                   
  ...npm-update.ts |   86.32 |    76.05 |     100 |   86.32 | ...02-303,331-341 
  math.ts          |       0 |        0 |       0 |       0 | 1-15              
  ...er-mention.ts |     100 |    66.66 |     100 |     100 | 14,30,44-46       
  ...iagnostics.ts |   94.57 |    83.01 |   88.88 |   94.57 | ...05,311,315-317 
  ...serMessage.ts |     100 |      100 |     100 |     100 |                   
  ...onfigUtils.ts |   94.25 |    91.17 |     100 |   94.25 | ...30,436,439-443 
  ...iveHelpers.ts |   95.13 |    91.79 |     100 |   95.13 | ...53-454,552,565 
  osc.ts           |   97.18 |      100 |    87.5 |   97.18 | 182-183           
  package.ts       |   88.88 |       80 |     100 |   88.88 | 31-32             
  ...uggestions.ts |   84.29 |    70.83 |     100 |   84.29 | 70-76,92-103      
  processUtils.ts  |    92.3 |       80 |     100 |    92.3 | 45-46             
  readStdin.ts     |   93.67 |    94.11 |   85.71 |   93.67 | 79-83             
  relaunch.ts      |   95.87 |    89.28 |     100 |   95.87 | 103-105,131       
  resolvePath.ts   |     100 |      100 |     100 |     100 |                   
  runBudget.ts     |   99.35 |    96.77 |     100 |   99.35 | 119               
  sandbox-path.ts  |     100 |      100 |     100 |     100 |                   
  sandbox.ts       |   45.67 |    56.93 |   76.92 |   45.67 | ...1034,1046-1069 
  ...xImageName.ts |     100 |    77.77 |     100 |     100 | 10,18             
  sandboxMounts.ts |     100 |      100 |     100 |     100 |                   
  sessionPaths.ts  |   90.84 |    90.56 |     100 |   90.84 | ...81-182,185-186 
  settingsUtils.ts |   82.35 |    89.57 |      90 |   82.35 | ...25-743,750-758 
  spawnWrapper.ts  |     100 |      100 |     100 |     100 |                   
  ...ate-verify.ts |     100 |      100 |     100 |     100 |                   
  ...one-update.ts |   39.81 |    77.44 |   62.16 |   39.81 | ...1193,1196-1215 
  ...upProfiler.ts |   98.47 |    94.66 |     100 |   98.47 | 132-133,308       
  ...upWarnings.ts |     100 |      100 |     100 |     100 |                   
  stdioHelpers.ts  |     100 |     87.5 |     100 |     100 | 23                
  systemInfo.ts    |   95.12 |     89.7 |     100 |   95.12 | ...54-255,260-264 
  ...InfoFields.ts |    87.5 |    65.85 |     100 |    87.5 | ...24-125,146-147 
  ...alSequence.ts |     100 |    97.61 |     100 |     100 | 60                
  ...iffPreview.ts |   76.47 |       25 |     100 |   76.47 | 13,17,23-24       
  ...on-handler.ts |    73.8 |       75 |     100 |    73.8 | 17-18,25-26,67-73 
  ...e-relaunch.ts |   89.61 |    86.66 |      50 |   89.61 | 56-61,83-84       
  ...entEmitter.ts |     100 |      100 |     100 |     100 |                   
  ...ansionHook.ts |     100 |      100 |     100 |     100 |                   
  ...upWarnings.ts |   87.75 |       75 |     100 |   87.75 | 47-48,53-54,57-58 
  version.ts       |     100 |       50 |     100 |     100 | 11                
  ...ingHandler.ts |     100 |      100 |     100 |     100 |                   
  windowTitle.ts   |   95.45 |    93.33 |     100 |   95.45 | 54-55             
  ...WithBackup.ts |   65.04 |    77.77 |     100 |   65.04 | 97,112,133-172    
 ...s/housekeeping |   91.63 |    91.02 |      95 |   91.63 |                   
  cleanup.ts       |   95.77 |    95.83 |     100 |   95.77 | 70-72             
  ...eractionAt.ts |     100 |      100 |     100 |     100 |                   
  scheduler.ts     |   91.91 |    90.47 |    87.5 |   91.91 | 58-62,73,131-135  
  throttledOnce.ts |   86.66 |     86.2 |     100 |   86.66 | ...99,105,137-138 
-------------------|---------|----------|---------|---------|-------------------
Core Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |   87.56 |     86.2 |   89.15 |   87.56 |                   
 src               |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/__mocks__/fs  |       0 |        0 |       0 |       0 |                   
  promises.ts      |       0 |        0 |       0 |       0 | 1-48              
 src/agents        |   90.27 |    83.81 |   95.56 |   90.27 |                   
  ...transcript.ts |   87.63 |    83.52 |     100 |   87.63 | ...80,588,594-598 
  ...ent-resume.ts |   85.53 |    77.44 |   83.33 |   85.53 | ...1781-1785,1788 
  ...ound-tasks.ts |   96.15 |    90.13 |   98.76 |   96.15 | ...1732,1752-1755 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...ent-result.ts |    96.8 |    92.68 |     100 |    96.8 | 106,129-131       
  ...n-registry.ts |   92.47 |    84.74 |     100 |   92.47 | ...98-904,909-911 
  ...w-snapshot.ts |   91.86 |    75.75 |     100 |   91.86 | ...54,178,185-187 
 src/agents/arena  |   76.32 |    67.71 |   78.94 |   76.32 |                   
  ...gentClient.ts |   79.47 |    88.88 |   81.81 |   79.47 | ...68-183,189-204 
  ArenaManager.ts  |   75.11 |    64.51 |   78.57 |   75.11 | ...1887,1893-1894 
  arena-events.ts  |   64.44 |      100 |      50 |   64.44 | ...71-175,178-183 
  diff-summary.ts  |    87.5 |    72.34 |     100 |    87.5 | ...32-133,137-138 
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...gents/backends |   78.09 |    85.23 |   76.28 |   78.09 |                   
  ITermBackend.ts  |   97.97 |    93.93 |     100 |   97.97 | ...78-180,255,307 
  ...essBackend.ts |    90.9 |    85.36 |   93.33 |    90.9 | ...70,672,674-675 
  TmuxBackend.ts   |    90.7 |    76.55 |   97.36 |    90.7 | ...87,697,743-747 
  detect.ts        |   31.25 |      100 |       0 |   31.25 | 34-88             
  index.ts         |     100 |      100 |     100 |     100 |                   
  iterm-it2.ts     |     100 |     92.1 |     100 |     100 | 37-38,106         
  tmux-commands.ts |    6.64 |      100 |    3.03 |    6.64 | ...93-363,386-503 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...agents/runtime |   90.49 |    85.89 |   87.64 |   90.49 |                   
  agent-context.ts |     100 |      100 |     100 |     100 |                   
  agent-core.ts    |   85.05 |    76.67 |   77.77 |   85.05 | ...2287,2333-2335 
  agent-events.ts  |     100 |      100 |     100 |     100 |                   
  ...t-headless.ts |   93.49 |    89.41 |   83.33 |   93.49 | ...96-497,500-501 
  ...nteractive.ts |   81.01 |    82.35 |   76.66 |   81.01 | ...33,535-538,541 
  ...statistics.ts |   98.29 |    82.55 |     100 |   98.29 | 141,165,206,239   
  agent-types.ts   |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...ool-policy.ts |   98.34 |      100 |    92.3 |   98.34 | 81-82             
  ...low-budget.ts |     100 |      100 |     100 |     100 |                   
  ...ow-journal.ts |   91.76 |    75.86 |     100 |   91.76 | ...38-139,179-181 
  ...chestrator.ts |   91.86 |    88.71 |   82.35 |   91.86 | ...1782,1831-1834 
  ...ow-prompts.ts |     100 |      100 |     100 |     100 |                   
  ...low-runner.ts |   93.14 |    82.25 |   91.66 |   93.14 | ...56,200,220-223 
  ...ow-sandbox.ts |   96.87 |    94.64 |     100 |   96.87 | ...24-325,330-331 
  ...flow-saved.ts |   96.51 |    94.36 |     100 |   96.51 | 134-135,234-237   
  ...flow-stall.ts |    97.9 |    83.33 |     100 |    97.9 | 138-139,236       
 src/agents/tasks  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/agents/team   |   81.81 |    83.99 |    87.5 |   81.81 |                   
  TeamManager.ts   |   72.02 |    79.41 |   79.24 |   72.02 | ...1632,1655-1656 
  identity.ts      |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...sionBridge.ts |     100 |      100 |     100 |     100 |                   
  mailbox.ts       |   94.76 |    86.36 |   92.85 |   94.76 | 86-87,348-354     
  ...ptAddendum.ts |     100 |      100 |     100 |     100 |                   
  tasks.ts         |   88.85 |    82.56 |   96.29 |   88.85 | ...-990,1034-1035 
  team-events.ts   |   60.52 |      100 |      50 |   60.52 | ...40-144,151-155 
  teamHelpers.ts   |   92.02 |    94.91 |   95.23 |   92.02 | ...31-332,368-378 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...eam/test-utils |   94.39 |    94.26 |   98.21 |   94.39 |                   
  ...on-harness.ts |   96.49 |    84.21 |     100 |   96.49 | 128-129,141-142   
  fake-agent.ts    |   98.49 |    95.08 |     100 |   98.49 | 201-203           
  fake-backend.ts  |   86.46 |    97.61 |   95.83 |   86.46 | 124-146           
 src/config        |   84.92 |    87.02 |   75.21 |   84.92 |                   
  approval-mode.ts |     100 |      100 |     100 |     100 |                   
  ...xtDefaults.ts |     100 |      100 |     100 |     100 |                   
  config.ts        |   84.22 |    86.72 |   73.59 |   84.22 | ...8194,8198-8199 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  models.ts        |     100 |      100 |     100 |     100 |                   
  storage.ts       |   94.39 |    91.57 |   88.23 |   94.39 | ...45-446,449-450 
 ...nfirmation-bus |   98.27 |    97.14 |     100 |   98.27 |                   
  message-bus.ts   |   98.14 |    97.05 |     100 |   98.14 | 42-43             
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/core          |   91.97 |    88.25 |   93.21 |   91.97 |                   
  baseLlmClient.ts |   88.28 |    82.48 |   81.81 |   88.28 | ...47,660,666-668 
  client.ts        |   91.92 |    87.39 |   91.56 |   91.92 | ...3915,4011-4012 
  ...tGenerator.ts |   86.34 |    87.34 |   84.61 |   86.34 | ...81-482,527-533 
  ...lScheduler.ts |    90.1 |    85.95 |      96 |    90.1 | ...5644,5672-5683 
  geminiChat.ts    |   92.57 |    89.36 |      96 |   92.57 | ...4508,4556-4557 
  geminiRequest.ts |     100 |      100 |     100 |     100 |                   
  genai-compat.ts  |     100 |      100 |     100 |     100 |                   
  ...MediaLimit.ts |     100 |       96 |     100 |     100 | 96                
  ...htProtocol.ts |    9.09 |      100 |       0 |    9.09 | ...9,62-66,69-110 
  ...ream-error.ts |     100 |      100 |     100 |     100 |                   
  logger.ts        |   87.41 |    87.02 |     100 |   87.41 | ...64-568,614-628 
  ...lay-buffer.ts |     100 |      100 |     100 |     100 |                   
  ...dispatcher.ts |     100 |      100 |     100 |     100 |                   
  ...tyDefaults.ts |     100 |      100 |     100 |     100 |                   
  ...olExecutor.ts |   93.54 |    83.33 |      50 |   93.54 | 49-50             
  ...on-helpers.ts |   93.49 |    78.57 |     100 |   93.49 | ...10-211,228-229 
  ...issionFlow.ts |   98.97 |    96.96 |     100 |   98.97 | 107               
  ...try-policy.ts |     100 |      100 |     100 |     100 |                   
  ...ell-policy.ts |   95.19 |    89.47 |     100 |   95.19 | ...44-245,290-291 
  prompts.ts       |   93.57 |    91.42 |   83.33 |   93.57 | ...1187,1390-1391 
  ...ing-effort.ts |     100 |      100 |     100 |     100 |                   
  ...n-recovery.ts |   95.13 |       80 |     100 |   95.13 | ...06-107,142-144 
  ...t-profiler.ts |    97.9 |    81.15 |   88.23 |    97.9 | 117,124-125,130   
  ...port-retry.ts |     100 |      100 |     100 |     100 |                   
  tokenLimits.ts   |     100 |     92.1 |     100 |     100 | 87,122-139        
  ...reparation.ts |     100 |      100 |     100 |     100 |                   
  ...tion-guard.ts |     100 |      100 |     100 |     100 |                   
  ...allIdUtils.ts |   98.41 |    93.47 |     100 |   98.41 | 36,45             
  ...okTriggers.ts |   99.45 |    92.43 |     100 |   99.45 | 182,193           
  ...terruption.ts |     100 |     92.3 |     100 |     100 | 86,104            
  turn.ts          |   98.52 |    91.66 |     100 |   98.52 | ...14,642-643,690 
  ...l-fallback.ts |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   96.32 |    88.02 |    96.1 |   96.32 |                   
  ...tGenerator.ts |   97.23 |    86.48 |   94.73 |   97.23 | ...1423,1452,1463 
  converter.ts     |   96.19 |    89.25 |     100 |   96.19 | ...1329,1550-1552 
  index.ts         |       0 |        0 |       0 |       0 | 1-21              
  usage.ts         |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   88.78 |    72.36 |   89.47 |   88.78 |                   
  ...tGenerator.ts |   87.18 |    71.83 |   88.88 |   87.18 | ...58-364,382-383 
  index.ts         |     100 |       80 |     100 |     100 | 50                
 ...ntentGenerator |    95.6 |    88.74 |    92.3 |    95.6 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tGenerator.ts |   95.52 |    87.88 |   91.89 |   95.52 | ...1195-1196,1224 
  ...tDetection.ts |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   91.65 |    90.23 |   95.23 |   91.65 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  converter.ts     |   91.15 |    89.32 |   96.87 |   91.15 | ...1914,2083-2098 
  errorHandler.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |   60.31 |       75 |      50 |   60.31 | ...71,74-78,90-94 
  ...tGenerator.ts |    66.4 |    70.58 |   88.88 |    66.4 | ...51-157,168-169 
  pipeline.ts      |   96.63 |    90.94 |     100 |   96.63 | ...1100,1108,1203 
  ...ureContext.ts |     100 |      100 |     100 |     100 |                   
  ...ingOptions.ts |       0 |        0 |       0 |       0 | 1                 
  ...CallParser.ts |   92.24 |     92.4 |     100 |   92.24 | ...28-529,549-552 
  ...kingParser.ts |     100 |    96.87 |     100 |     100 | 42                
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...rator/provider |   96.73 |    89.76 |   98.27 |   96.73 |                   
  dashscope.ts     |   97.48 |    91.91 |      95 |   97.48 | ...85-386,528-529 
  deepseek.ts      |   94.91 |    89.36 |     100 |   94.91 | ...31-132,145-146 
  default.ts       |   99.16 |    96.96 |     100 |   99.16 | 198               
  index.ts         |     100 |      100 |     100 |     100 |                   
  mimo.ts          |   94.11 |    66.66 |     100 |   94.11 | 29,52-53          
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  mistral.ts       |   96.07 |    73.33 |     100 |   96.07 | 32-33             
  modelscope.ts    |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 |                   
  utils.ts         |     100 |      100 |     100 |     100 |                   
  zai.ts           |   92.13 |    82.14 |     100 |   92.13 | ...,39-40,135-137 
 src/extension     |    86.2 |    83.25 |   92.33 |    86.2 |                   
  ...ive-safety.ts |     100 |      100 |     100 |     100 |                   
  ...-converter.ts |   78.32 |    71.83 |     100 |   78.32 | ...1122,1168-1169 
  corruptFile.ts   |     100 |       50 |     100 |     100 | 40-45             
  ...-converter.ts |   80.39 |     87.5 |     100 |   80.39 | 50-59             
  ...me-refresh.ts |     100 |      100 |     100 |     100 |                   
  ...sion-store.ts |   90.82 |    86.38 |   97.82 |   90.82 | ...1215-1221,1265 
  ...ionManager.ts |   81.06 |    78.78 |   81.52 |   81.06 | ...2705,2727-2728 
  ...references.ts |     100 |     90.9 |     100 |     100 | ...05,129,197,200 
  ...onSettings.ts |    92.3 |     94.4 |     100 |    92.3 | ...98-501,570-571 
  ...-converter.ts |    75.9 |    84.61 |   85.71 |    75.9 | ...98,202,214-248 
  github.ts        |   88.58 |    82.13 |     100 |   88.58 | ...62,952-953,963 
  http-client.ts   |   84.61 |       80 |     100 |   84.61 | 20-21             
  i18n.ts          |   78.26 |       96 |      50 |   78.26 | 104-110,116-123   
  index.ts         |     100 |      100 |     100 |     100 |                   
  marketplace.ts   |   88.39 |    83.11 |     100 |   88.39 | ...08,494,507-508 
  ...ork-policy.ts |   89.72 |       90 |     100 |   89.72 | ...36,148-154,156 
  npm.ts           |   89.02 |    81.81 |     100 |   89.02 | ...86-688,695-700 
  override.ts      |   94.11 |    93.33 |     100 |   94.11 | 63-64,81-82       
  redaction.ts     |     100 |      100 |     100 |     100 |                   
  settings.ts      |   66.26 |      100 |      50 |   66.26 | 81-107,141-146    
  ...ceRegistry.ts |   94.01 |    83.14 |     100 |   94.01 | ...38-344,365-366 
  storage.ts       |     100 |      100 |     100 |     100 |                   
  ...ableSchema.ts |     100 |      100 |     100 |     100 |                   
  variables.ts     |   88.95 |    83.78 |     100 |   88.95 | ...32-235,238-241 
  ...extraction.ts |   85.77 |    80.61 |   89.47 |   85.77 | ...02-205,260-261 
 src/followup      |   79.86 |    80.36 |    90.9 |   79.86 |                   
  followupState.ts |   98.44 |    95.74 |     100 |   98.44 | 236-237           
  index.ts         |     100 |      100 |     100 |     100 |                   
  overlayFs.ts     |   96.29 |    88.88 |     100 |   96.29 | 78,108,122        
  speculation.ts   |   71.45 |    66.01 |   71.42 |   71.45 | ...49-650,657-658 
  ...onToolGate.ts |     100 |    96.55 |     100 |     100 | 97                
  ...nGenerator.ts |   72.03 |    81.15 |   83.33 |   72.03 | ...68-219,331-333 
 src/generated     |       0 |        0 |       0 |       0 |                   
  git-commit.ts    |       0 |        0 |       0 |       0 | 1-10              
 src/goals         |   94.17 |    88.82 |   95.93 |   94.17 |                   
  ...eGoalStore.ts |   87.61 |    88.88 |   86.66 |   87.61 | ...85-188,196-204 
  goal-evidence.ts |   87.59 |     85.1 |   95.65 |   87.59 | ...12-613,636-639 
  ...projection.ts |   89.41 |    72.22 |   66.66 |   89.41 | ...28,131,135-137 
  ...ersistence.ts |   87.73 |    84.84 |      80 |   87.73 | ...-94,97,101-106 
  goal-protocol.ts |    91.3 |       90 |     100 |    91.3 | 107-108           
  goal-reducer.ts  |   92.21 |    85.48 |     100 |   92.21 | ...67-368,381,436 
  goal-runtime.ts  |   99.01 |    93.44 |     100 |   99.01 | ...83-684,707-708 
  goal-tools.ts    |   98.32 |    93.18 |   95.23 |   98.32 | ...48-149,256-257 
  ...rn-context.ts |     100 |      100 |     100 |     100 |                   
  goal-verifier.ts |   92.46 |    92.85 |     100 |   92.46 | ...67-170,183-185 
  goal-wire.ts     |       0 |        0 |       0 |       0 | 1-27              
  goalHook.ts      |   96.91 |    92.42 |     100 |   96.91 | 115-120,221-222   
  goalJudge.ts     |   95.84 |    87.09 |     100 |   95.84 | ...55-356,448-449 
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/hooks         |   88.11 |    86.38 |   88.62 |   88.11 |                   
  ...okRegistry.ts |   86.48 |    77.08 |     100 |   86.48 | ...41-344,362-369 
  ...bortSignal.ts |     100 |      100 |     100 |     100 |                   
  context-usage.ts |     100 |      100 |     100 |     100 |                   
  ...terpolator.ts |   96.66 |    93.33 |     100 |   96.66 | 66-67             
  ...HookRunner.ts |   96.68 |    87.23 |     100 |   96.68 | 110-112,231-233   
  ...Aggregator.ts |   96.57 |    91.48 |     100 |   96.57 | ...20-321,402,404 
  ...entHandler.ts |   95.57 |    84.76 |   94.73 |   95.57 | ...1040-1041,1051 
  hookPlanner.ts   |   87.55 |    85.54 |   86.66 |   87.55 | ...22-226,233-244 
  hookRegistry.ts  |   92.53 |    85.43 |     100 |   92.53 | ...39,458,462,466 
  hookRunner.ts    |   62.65 |    72.34 |   66.66 |   62.65 | ...70-771,780-781 
  hookSystem.ts    |   87.64 |     98.5 |   70.83 |   87.64 | ...58-759,765-766 
  ...HookRunner.ts |   79.06 |    66.66 |      80 |   79.06 | ...33-434,452-456 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...edCallback.ts |     100 |      100 |     100 |     100 |                   
  ...HookRunner.ts |   94.19 |    84.37 |   81.81 |   94.19 | ...76-384,458-459 
  ...SkillHooks.ts |   78.75 |       75 |   66.66 |   78.75 | 62-66,137-152     
  ...oksManager.ts |   94.87 |    88.88 |     100 |   94.87 | ...84,325,327-329 
  ssrfGuard.ts     |   86.45 |    89.13 |     100 |   86.45 | ...85,289-295,301 
  stopHookCap.ts   |     100 |      100 |     100 |     100 |                   
  trustedHooks.ts  |      90 |    52.63 |     100 |      90 | ...53,66-67,97-98 
  types.ts         |   94.25 |    96.12 |   88.88 |   94.25 | ...46-547,632-636 
  urlValidator.ts  |     100 |      100 |     100 |     100 |                   
  ...it-context.ts |     100 |      100 |     100 |     100 |                   
 src/ide           |   76.98 |    85.03 |   79.03 |   76.98 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  detect-ide.ts    |     100 |      100 |     100 |     100 |                   
  ide-client.ts    |   69.16 |    84.65 |   68.29 |   69.16 | ...1068,1097-1105 
  ide-installer.ts |   89.06 |    79.31 |     100 |   89.06 | ...36,143-147,160 
  ideContext.ts    |     100 |      100 |     100 |     100 |                   
  process-utils.ts |   84.84 |    71.79 |     100 |   84.84 | ...37,151,193-194 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/lsp           |   58.96 |    70.57 |   66.14 |   58.96 |                   
  ...nfigLoader.ts |   80.55 |       72 |   95.45 |   80.55 | ...02-504,508-514 
  ...ionFactory.ts |   42.81 |    73.07 |      50 |   42.81 | ...76-427,433-450 
  ...Normalizer.ts |   23.09 |    13.72 |   30.43 |   23.09 | ...04-905,909-924 
  ...verManager.ts |   75.73 |     80.1 |   79.66 |   75.73 | ...1346,1352-1382 
  ...eLspClient.ts |   32.78 |       80 |   16.66 |   32.78 | ...89-293,299-300 
  ...LspService.ts |      60 |    73.36 |   78.26 |      60 | ...1575,1635-1645 
  configHash.ts    |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/mcp           |    82.3 |    77.81 |   78.33 |    82.3 |                   
  configHash.ts    |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...h-provider.ts |   86.95 |      100 |   33.33 |   86.95 | ...,93,97,101-102 
  ...h-provider.ts |   79.31 |    58.06 |     100 |   79.31 | ...26-933,940-942 
  ...en-storage.ts |   98.78 |    97.95 |     100 |   98.78 | 106-107           
  oauth-utils.ts   |   73.61 |    85.48 |    92.3 |   73.61 | ...46-366,392-421 
  ...n-provider.ts |   89.83 |       96 |   45.45 |   89.83 | ...43,147,151-152 
 .../token-storage |   82.12 |    88.19 |   89.28 |   82.12 |                   
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   87.08 |    87.03 |   95.23 |   87.08 | ...00-201,214-215 
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   68.14 |    82.35 |   64.28 |   68.14 | ...81-295,298-314 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/memory        |   87.42 |    83.29 |   90.44 |   87.42 |                   
  ...y-document.ts |   89.52 |    84.61 |     100 |   89.52 | ...24-325,329-330 
  ...nel-memory.ts |   97.11 |    95.72 |   96.29 |   97.11 | ...85-287,361-362 
  const.ts         |   94.28 |     92.3 |     100 |   94.28 | 66-67             
  dream.ts         |    64.6 |    72.22 |      50 |    64.6 | ...04-109,124-165 
  ...entPlanner.ts |     100 |    81.81 |     100 |     100 | 136,146           
  entries.ts       |   75.59 |    84.84 |   83.33 |   75.59 | ...56-157,172-180 
  extract.ts       |   91.48 |    75.75 |     100 |   91.48 | ...99,118-121,189 
  ...entPlanner.ts |   91.59 |    76.19 |     100 |   91.59 | ...05,114-117,293 
  ...ionPlanner.ts |       0 |        0 |       0 |       0 | 1                 
  forget.ts        |   81.83 |       75 |   83.33 |   81.83 | ...51,474,478-507 
  indexer.ts       |   94.14 |       84 |     100 |   94.14 | ...32-233,334,337 
  ...kill-agent.ts |   97.94 |    89.36 |     100 |   97.94 | 82-83,179-180     
  manager.ts       |    78.4 |    82.29 |   77.77 |    78.4 | ...1482,1495-1497 
  ...ent-config.ts |   86.95 |    82.52 |   86.36 |   86.95 | ...68,388,395-401 
  memoryAge.ts     |   90.47 |       80 |     100 |   90.47 | 50-51             
  paths.ts         |   95.29 |    96.59 |     100 |   95.29 | ...80-381,402-403 
  ...ing-skills.ts |     100 |       72 |     100 |     100 | 31-35,73-78,97    
  prompt.ts        |   97.26 |    87.03 |     100 |   97.26 | ...10-218,222,225 
  recall.ts        |   82.06 |       75 |    90.9 |   82.06 | ...59-364,395-406 
  refresh.ts       |   89.85 |    82.92 |     100 |   89.85 | ...54-155,162-163 
  ...ceSelector.ts |    93.1 |    81.81 |     100 |    93.1 | ...25,127-128,136 
  remember.ts      |   98.89 |    89.79 |     100 |   98.89 | 50,70             
  scan.ts          |   93.12 |    77.41 |     100 |   93.12 | ...08-109,154,157 
  scopes.ts        |     100 |      100 |     100 |     100 |                   
  ...et-scanner.ts |     100 |      100 |     100 |     100 |                   
  ...entPlanner.ts |   71.68 |    65.51 |   68.75 |   71.68 | ...90-394,397,403 
  status.ts        |   10.52 |      100 |       0 |   10.52 | 41-98             
  store.ts         |   92.92 |     82.6 |     100 |   92.92 | ...16-117,147-148 
  ...git-status.ts |     100 |     87.5 |     100 |     100 | 30                
  ...cret-guard.ts |     100 |      100 |     100 |     100 |                   
  ...emory-sync.ts |   94.24 |    82.85 |     100 |   94.24 | ...34-236,246-247 
  types.ts         |     100 |      100 |     100 |     100 |                   
  ...ontextFile.ts |   81.21 |    81.53 |   81.81 |   81.21 | ...63-277,291-296 
 src/mocks         |       0 |        0 |       0 |       0 |                   
  msw.ts           |       0 |        0 |       0 |       0 | 1-9               
 src/models        |   92.54 |       89 |   91.13 |   92.54 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...tor-config.ts |   97.77 |    91.83 |     100 |   97.77 | 155,161,171       
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...nfigErrors.ts |   74.22 |    47.82 |   84.61 |   74.22 | ...,67-74,106-117 
  ...igResolver.ts |   98.71 |    93.33 |     100 |   98.71 | 166,328,334       
  modelRegistry.ts |     100 |    98.13 |     100 |     100 | 177,260           
  modelsConfig.ts  |   89.36 |    86.93 |   88.09 |   89.36 | ...1404,1433-1434 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/output        |     100 |      100 |     100 |     100 |                   
  ...-formatter.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/permissions   |   83.77 |    91.27 |   71.07 |   83.77 |                   
  autoMode.ts      |   97.66 |    93.13 |     100 |   97.66 | ...82-589,635,712 
  ...transcript.ts |      98 |    84.61 |     100 |      98 | 200-201           
  classifier.ts    |      94 |    94.54 |     100 |      94 | 158-165,389-393   
  ...erousRules.ts |     100 |    89.36 |     100 |     100 | 110,133,147,175   
  ...alTracking.ts |     100 |      100 |     100 |     100 |                   
  ...e-commands.ts |   86.77 |     73.8 |     100 |   86.77 | 131-141,210-214   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...on-manager.ts |   86.54 |    89.63 |      80 |   86.54 | ...1096,1202-1206 
  rule-parser.ts   |   94.49 |    92.72 |     100 |   94.49 | ...1447,1481-1483 
  ...-semantics.ts |   70.44 |    91.07 |   46.66 |   70.44 | ...2237,2311-2314 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...sifier-prompts |   99.04 |    95.23 |     100 |   99.04 |                   
  system-prompt.ts |   99.04 |    95.23 |     100 |   99.04 | 220               
 src/prompts       |   83.63 |      100 |    87.5 |   83.63 |                   
  mcp-prompts.ts   |   18.18 |      100 |       0 |   18.18 | 11-19             
  ...t-registry.ts |     100 |      100 |     100 |     100 |                   
 src/providers     |   83.71 |     78.5 |   81.25 |   83.71 |                   
  all-providers.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  install.ts       |   93.11 |     84.5 |     100 |   93.11 | ...56-257,330-331 
  ...der-config.ts |   75.85 |    73.84 |   78.26 |   75.85 | ...73-474,502-503 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...viders/presets |   97.82 |    91.66 |   63.63 |   97.82 |                   
  ...oding-plan.ts |   87.34 |      100 |       0 |   87.34 | 82-84,87-89,91-94 
  ...a-standard.ts |     100 |      100 |     100 |     100 |                   
  ...token-plan.ts |     100 |      100 |     100 |     100 |                   
  ...m-provider.ts |   97.05 |    81.25 |      75 |   97.05 | 118-119           
  deepseek.ts      |     100 |      100 |     100 |     100 |                   
  grok.ts          |     100 |      100 |     100 |     100 |                   
  idealab.ts       |     100 |      100 |     100 |     100 |                   
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  modelscope.ts    |     100 |      100 |     100 |     100 |                   
  openrouter.ts    |     100 |      100 |     100 |     100 |                   
  requesty.ts      |     100 |      100 |     100 |     100 |                   
  zai.ts           |     100 |      100 |     100 |     100 |                   
 src/qwen          |   85.41 |    78.52 |   95.89 |   85.41 |                   
  ...tGenerator.ts |   98.64 |    98.18 |     100 |   98.64 | 105-106           
  qwenOAuth2.ts    |   82.79 |    73.29 |   90.62 |   82.79 | ...1205-1221,1251 
  ...kenManager.ts |   85.36 |    76.61 |     100 |   85.36 | ...52-757,778-783 
 src/resources     |     100 |      100 |     100 |     100 |                   
  ...e-registry.ts |     100 |      100 |     100 |     100 |                   
 src/services      |   89.55 |    84.32 |   96.93 |   89.55 |                   
  ...ionTrailer.ts |     100 |      100 |     100 |     100 |                   
  ...llRegistry.ts |   97.68 |    85.71 |     100 |   97.68 | ...96,119,490-491 
  ...ionService.ts |   96.71 |    95.79 |     100 |   96.71 | ...83,699,832-840 
  ...ingService.ts |   90.91 |    84.17 |   95.45 |   90.91 | ...2058,2085-2086 
  ...ttribution.ts |   91.73 |    87.71 |      90 |   91.73 | ...80-685,826-827 
  ...utSlimming.ts |    97.2 |    94.05 |     100 |    97.2 | ...39-340,378-381 
  cronScheduler.ts |   94.17 |    90.45 |      98 |   94.17 | ...1333,1736-1737 
  cronTasksFile.ts |   96.31 |    91.81 |     100 |   96.31 | ...11,336-337,483 
  cronTasksLock.ts |   94.44 |    89.47 |     100 |   94.44 | ...02-103,132-133 
  ...eryService.ts |   96.22 |    93.54 |      90 |   96.22 | 121,155-156,161   
  ...oryService.ts |   88.17 |    79.02 |    92.3 |   88.17 | ...1303,1344-1347 
  fileReadCache.ts |     100 |      100 |     100 |     100 |                   
  ...temService.ts |    92.8 |    84.68 |   94.11 |    92.8 | ...41,467-474,519 
  ...ratedFiles.ts |      96 |    88.23 |     100 |      96 | 119-120,146-147   
  gitInit.ts       |     100 |      100 |     100 |     100 |                   
  ...reeService.ts |    73.7 |    68.49 |   95.83 |    73.7 | ...2196,2225-2226 
  ...on-service.ts |   87.38 |       72 |     100 |   87.38 | ...01-305,343-344 
  ...references.ts |   98.39 |    88.76 |     100 |   98.39 | 154-155,215-216   
  ...ionService.ts |   98.22 |    97.34 |     100 |   98.22 | ...75-676,723-724 
  ...ticsDumper.ts |   98.37 |    95.23 |     100 |   98.37 | 185-186           
  ...ureMonitor.ts |   95.82 |    90.52 |   97.05 |   95.82 | ...60,861,875-877 
  ...orRegistry.ts |    97.3 |    91.22 |     100 |    97.3 | ...53-454,611-612 
  ...ttachments.ts |   97.74 |     90.8 |     100 |   97.74 | 298-308,646       
  ...ersistence.ts |   90.95 |    78.75 |     100 |   90.95 | ...78,963-964,992 
  ...on-service.ts |   94.49 |    92.26 |   97.14 |   94.49 | ...98-600,656-664 
  ...ce-service.ts |   98.38 |    93.75 |   88.88 |   98.38 | 63-64             
  ...ipt-reader.ts |   94.52 |    89.21 |   98.03 |   94.52 | ...1111-1112,1176 
  ...est-helper.ts |       0 |        0 |       0 |       0 | 1-65              
  ...iter-lease.ts |   82.81 |    74.08 |    97.5 |   82.81 | ...2364,2376-2379 
  sessionRecap.ts  |   67.56 |    43.47 |     100 |   67.56 | ...60,178,180-183 
  ...ionService.ts |   88.94 |    83.69 |   97.14 |   88.94 | ...2450,2520-2540 
  sessionTitle.ts  |   94.19 |    73.21 |     100 |   94.19 | ...43-246,277-278 
  ...ionService.ts |   84.35 |    78.37 |   97.14 |   84.35 | ...2472,2478-2483 
  ...pInhibitor.ts |   97.42 |    92.77 |     100 |   97.42 | ...30,169,369-370 
  ...Estimation.ts |     100 |    88.23 |     100 |     100 | 118-119           
  ...ageService.ts |   97.76 |    91.59 |   93.75 |   97.76 | ...61-262,366,567 
  ...UseSummary.ts |   94.63 |    88.46 |     100 |   94.63 | ...62-164,214-215 
  ...rd-service.ts |     100 |    88.37 |     100 |     100 | ...29,145-146,241 
  ...oryService.ts |   90.72 |    84.07 |     100 |   90.72 | ...06-509,561-562 
  ...reeCleanup.ts |   14.42 |      100 |   33.33 |   14.42 | 58-186            
  ...ionService.ts |   87.98 |    86.95 |     100 |   87.98 | ...38-439,455-456 
 ...icrocompaction |   99.41 |    96.55 |     100 |   99.41 |                   
  microcompact.ts  |   99.41 |    96.55 |     100 |   99.41 | 244-245,677       
 ...s/visionBridge |   98.81 |    92.12 |     100 |   98.81 |                   
  ...capability.ts |     100 |      100 |     100 |     100 |                   
  ...part-utils.ts |     100 |      100 |     100 |     100 |                   
  ...ion-bridge.ts |   98.72 |    82.35 |     100 |   98.72 | 65,71             
  ...-constants.ts |     100 |      100 |     100 |     100 |                   
  ...ge-service.ts |   98.61 |     94.7 |     100 |   98.61 | ...06,666,679-680 
 src/skills        |   88.96 |    88.48 |   90.47 |   88.96 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...activation.ts |     100 |    93.33 |     100 |     100 | 93,112            
  skill-load.ts    |   94.84 |     87.5 |     100 |   94.84 | ...03,223,235-237 
  skill-manager.ts |   84.82 |    85.29 |   83.33 |   84.82 | ...1243,1250-1254 
  skill-paths.ts   |   89.65 |    86.95 |     100 |   89.65 | ...11-112,117-118 
  symlinkScope.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |   97.91 |       98 |     100 |   97.91 | 277-278           
 ...ataviz/scripts |   80.06 |    95.23 |   88.23 |   80.06 |                   
  ...te_palette.js |   80.06 |    95.23 |   88.23 |   80.06 | 261-296,306-328   
 ...s/bundled/loop |   97.48 |    95.77 |     100 |   97.48 |                   
  ...omous-loop.ts |     100 |      100 |     100 |     100 |                   
  ...-task-file.ts |   94.85 |     92.4 |     100 |   94.85 | ...56,367,375-376 
  ...k-resolver.ts |     100 |      100 |     100 |     100 |                   
 src/subagents     |   87.72 |    89.01 |   96.55 |   87.72 |                   
  ...ter-schema.ts |     100 |    98.07 |     100 |     100 | 99                
  ...tin-agents.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...nt-manager.ts |   84.48 |    85.91 |   94.87 |   84.48 | ...1582,1659-1660 
  types.ts         |     100 |      100 |     100 |     100 |                   
  validation.ts    |   92.46 |    95.18 |     100 |   92.46 | 47-52,63-68,71-76 
 src/telemetry     |   81.15 |    83.96 |   84.57 |   81.15 |                   
  ...ty-tracker.ts |     100 |      100 |     100 |     100 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...on-metrics.ts |   99.07 |    80.95 |     100 |   99.07 | 183,197           
  ...on-tracing.ts |   76.31 |    74.62 |   73.68 |   76.31 | ...80,387-389,405 
  ...attributes.ts |   95.15 |    87.27 |     100 |   95.15 | ...97-198,216-217 
  ...ag-metrics.ts |     100 |    77.77 |     100 |     100 | 21,40             
  ...t-loop-lag.ts |   96.85 |    85.71 |     100 |   96.85 | 170-173           
  ...-exporters.ts |   65.78 |    83.33 |   55.55 |   65.78 | ...04-105,108-109 
  ...ai-content.ts |    74.5 |    66.41 |   91.66 |    74.5 | ...1480,1493-1502 
  ...i-provider.ts |     100 |       99 |     100 |     100 | 99                
  ...ai-request.ts |   87.52 |    92.79 |   83.78 |   87.52 | ...55-561,564-570 
  gen-ai-usage.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-111             
  ...-processor.ts |   99.09 |    95.61 |      95 |   99.09 | 141,365-366       
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-128             
  loggers.ts       |   56.85 |    74.35 |   64.81 |   56.85 | ...1397,1414-1434 
  metrics.ts       |   79.63 |    81.98 |   79.66 |   79.63 | ...1082,1085-1096 
  otlp-urls.ts     |     100 |      100 |     100 |     100 |                   
  ...attributes.ts |     100 |      100 |     100 |     100 |                   
  ...ime-config.ts |       0 |        0 |       0 |       0 | 1                 
  sanitize.ts      |      80 |    83.33 |     100 |      80 | 35-36,41-42       
  ...rters-grpc.ts |     100 |      100 |     100 |     100 |                   
  ...rters-http.ts |     100 |      100 |     100 |     100 |                   
  sdk-impl.ts      |   91.06 |    87.15 |   68.75 |   91.06 | ...32,478-479,495 
  sdk.ts           |   79.22 |    89.18 |   63.63 |   79.22 | ...57-161,199-221 
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  ...on-tracing.ts |   90.83 |    90.05 |   96.77 |   90.83 | ...1667,1698-1701 
  ...etry-utils.ts |     100 |      100 |     100 |     100 |                   
  ...l-decision.ts |     100 |      100 |     100 |     100 |                   
  trace-context.ts |     100 |      100 |     100 |     100 |                   
  ...e-id-utils.ts |     100 |      100 |     100 |     100 |                   
  tracer.ts        |   98.56 |    88.63 |     100 |   98.56 | 52,101            
  types.ts         |   82.35 |    94.61 |   86.04 |   82.35 | ...1361,1365-1372 
  uiTelemetry.ts   |   97.18 |    93.93 |      88 |   97.18 | ...70,314,461-462 
 ...ry/qwen-logger |   74.17 |    80.35 |      70 |   74.17 |                   
  event-types.ts   |       0 |        0 |       0 |       0 |                   
  qwen-logger.ts   |   74.17 |    80.18 |   69.49 |   74.17 | ...1120,1158-1159 
 src/test-utils    |      94 |    98.24 |   78.94 |      94 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  ...st-helpers.ts |   94.11 |       90 |     100 |   94.11 | 69-70             
  index.ts         |     100 |      100 |     100 |     100 |                   
  mock-tool.ts     |   92.57 |      100 |   75.75 |   92.57 | ...63,227-228,241 
  ...aceContext.ts |     100 |      100 |     100 |     100 |                   
 src/tools         |   85.93 |    84.71 |   88.32 |   85.93 |                   
  ...erQuestion.ts |   89.71 |    80.76 |   91.66 |   89.71 | ...66-367,374-375 
  ...-registrar.ts |    77.7 |    66.66 |   66.66 |    77.7 | ...72-277,292-294 
  ...ub-session.ts |   89.67 |     91.3 |   81.81 |   89.67 | ...03-304,315-322 
  cron-create.ts   |   90.64 |    92.85 |   72.72 |   90.64 | ...,73-74,223-231 
  cron-delete.ts   |   97.56 |      100 |   83.33 |   97.56 | 31-32             
  cron-list.ts     |   98.23 |    95.34 |    87.5 |   98.23 | 57-58             
  diffOptions.ts   |     100 |      100 |     100 |     100 |                   
  edit.ts          |    82.7 |    86.77 |   81.25 |    82.7 | ...43-744,863-913 
  ...r-worktree.ts |   83.14 |    67.56 |    87.5 |   83.14 | ...84-187,278-279 
  enterPlanMode.ts |      85 |     82.6 |    87.5 |      85 | ...28-133,161-175 
  exit-worktree.ts |   83.29 |    83.65 |   94.44 |   83.29 | ...14-515,537-538 
  exitPlanMode.ts  |      95 |    85.29 |     100 |      95 | ...21-325,344,378 
  ...permission.ts |     100 |      100 |     100 |     100 |                   
  glob.ts          |   96.33 |     88.5 |     100 |   96.33 | ...24-225,373,376 
  grep.ts          |   90.73 |    86.61 |   85.71 |   90.73 | ...76-677,727-728 
  ...adTracking.ts |     100 |      100 |     100 |     100 |                   
  image-gen.ts     |   91.66 |    77.41 |    90.9 |   91.66 | ...13-214,221-222 
  list-agents.ts   |   94.02 |    82.35 |   83.33 |   94.02 | 31-32,47-48       
  loop-wakeup.ts   |   99.27 |    92.85 |     100 |   99.27 | 45                
  ls.ts            |   96.74 |    90.27 |     100 |   96.74 | 176-181,212,216   
  lsp.ts           |   72.71 |     59.5 |   90.32 |   72.71 | ...1212,1214-1215 
  ...nt-manager.ts |   81.49 |     80.1 |   85.71 |   81.49 | ...3217,3219-3220 
  mcp-client.ts    |   79.87 |    85.58 |   89.47 |   79.87 | ...2259,2263-2266 
  ...ry-timeout.ts |     100 |      100 |     100 |     100 |                   
  mcp-errors.ts    |     100 |      100 |     100 |     100 |                   
  ...pool-entry.ts |   77.56 |    84.11 |   77.14 |   77.56 | ...1291,1299-1300 
  ...ool-events.ts |       8 |        0 |       0 |       8 | 132-158           
  mcp-pool-key.ts  |   97.46 |    93.93 |     100 |   97.46 | 175-176           
  ...ce-content.ts |   96.55 |    91.17 |     100 |   96.55 | 80-82             
  mcp-retry.ts     |   97.67 |    95.65 |     100 |   97.67 | 131-132           
  mcp-status.ts    |     100 |      100 |     100 |     100 |                   
  mcp-tool.ts      |    97.2 |    93.47 |     100 |    97.2 | ...00-801,856-857 
  ...sport-pool.ts |   83.49 |    80.15 |   84.61 |   83.49 | ...1409,1416-1420 
  ...ace-budget.ts |   87.27 |     82.6 |     100 |   87.27 | ...00-305,340-345 
  memory-config.ts |     100 |      100 |     100 |     100 |                   
  ...iable-tool.ts |     100 |    84.61 |     100 |     100 | 101,108           
  monitor.ts       |   91.74 |    84.28 |   88.46 |   91.74 | ...93,606,804-809 
  notebook-edit.ts |   85.55 |    77.39 |   81.25 |   85.55 | ...86-902,948-949 
  ...escendants.ts |   36.17 |    64.51 |   55.55 |   36.17 | ...46-310,385-390 
  ...nforcement.ts |   82.57 |    90.24 |     100 |   82.57 | 174-185,234-247   
  read-file.ts     |   95.49 |    88.52 |   86.66 |   95.49 | ...49,464,536-537 
  ...p-resource.ts |   96.85 |      100 |   91.66 |   96.85 | 92-96             
  ...d-artifact.ts |   91.18 |    86.71 |    87.5 |   91.18 | ...26-427,441-453 
  ripGrep.ts       |    94.6 |    87.26 |   95.23 |    94.6 | ...33-734,740-741 
  ...-transport.ts |   71.42 |    55.55 |   71.42 |   71.42 | ...36-137,143-144 
  send-message.ts  |   81.13 |    89.74 |    62.5 |   81.13 | ...80-286,363-371 
  ...n-mcp-view.ts |   93.57 |     92.3 |      90 |   93.57 | 122-130           
  shell.ts         |   78.67 |    84.04 |   91.91 |   78.67 | ...5019,5082-5083 
  skill-utils.ts   |     100 |      100 |     100 |     100 |                   
  skill.ts         |   91.06 |    93.33 |   89.47 |   91.06 | ...71,475,520-542 
  ...eticOutput.ts |   95.12 |      100 |      80 |   95.12 | 87-88             
  task-create.ts   |    94.4 |    93.33 |   81.81 |    94.4 | 45-49,63-64,95    
  task-list.ts     |   73.38 |    77.77 |   83.33 |   73.38 | ...02,105,109-116 
  task-stop.ts     |   93.14 |    96.15 |   85.71 |   93.14 | 39-40,54-64       
  task-update.ts   |   82.89 |    83.92 |    92.3 |   82.89 | ...14-422,454-465 
  team-create.ts   |   97.22 |    85.71 |   83.33 |   97.22 | 48-49,129-130     
  team-delete.ts   |   86.74 |    83.33 |   83.33 |   86.74 | 37-38,42-48,72-73 
  ...n-approval.ts |   92.14 |    96.77 |   77.77 |   92.14 | 38-39,42-43,93-99 
  todoWrite.ts     |   94.36 |    84.61 |   92.85 |   94.36 | ...10-415,437-438 
  tool-error.ts    |     100 |      100 |     100 |     100 |                   
  tool-names.ts    |     100 |      100 |     100 |     100 |                   
  tool-registry.ts |   78.57 |    79.59 |    82.6 |   78.57 | ...89-990,998-999 
  tool-search.ts   |   96.19 |    89.72 |   93.33 |   96.19 | ...09,259-264,426 
  tools.ts         |   92.74 |    91.52 |    91.3 |   92.74 | ...63-564,580-586 
  ...reapproved.ts |   99.27 |    94.11 |     100 |   99.27 | 170               
  web-fetch.ts     |   96.05 |    90.54 |   96.77 |   96.05 | ...85-786,800-801 
  web-search.ts    |   90.53 |    83.57 |      80 |   90.53 | ...1007,1065-1068 
  write-file.ts    |    86.7 |    84.92 |   88.88 |    86.7 | ...24-827,864-899 
  zoom-image.ts    |   95.76 |    93.75 |      90 |   95.76 | 54-59,203-204     
 src/tools/agent   |   86.78 |    87.19 |   88.49 |   86.78 |                   
  agent.ts         |   85.41 |    86.28 |   86.31 |   85.41 | ...4286,4308-4318 
  fork-profile.ts  |   93.65 |       90 |     100 |   93.65 | ...33-134,171-174 
  fork-subagent.ts |    99.5 |    93.84 |     100 |    99.5 | 131               
 ...tools/artifact |   95.78 |    92.51 |   88.63 |   95.78 |                   
  artifact-tool.ts |   91.46 |    88.46 |   71.42 |   91.46 | ...13-314,322-325 
  ...-publisher.ts |     100 |    85.71 |     100 |     100 | 32                
  ...-publisher.ts |   96.74 |    97.72 |    87.5 |   96.74 | 29-30,156-157     
  html.ts          |     100 |    96.77 |     100 |     100 | 122               
  ...-publisher.ts |     100 |       80 |     100 |     100 | 30                
  oss-publisher.ts |    98.1 |    91.48 |     100 |    98.1 | 43-45             
  publisher.ts     |     100 |      100 |     100 |     100 |                   
 ...s/computer-use |   90.21 |    82.17 |   78.08 |   90.21 |                   
  bootstrap.ts     |   59.42 |    80.95 |   41.66 |   59.42 | ...35-339,341-345 
  client.ts        |   80.11 |       90 |   77.77 |   80.11 | ...97,242-243,274 
  constants.ts     |     100 |    94.73 |     100 |     100 | 129,256           
  downloader.ts    |   65.29 |    52.77 |   58.33 |   65.29 | ...99-300,316-355 
  index.ts         |     100 |      100 |     100 |     100 |                   
  install-state.ts |   94.44 |    72.72 |     100 |   94.44 | 44-45             
  ...n-detector.ts |     100 |     87.5 |     100 |     100 | 50                
  schemas.ts       |     100 |      100 |     100 |     100 |                   
  tool.ts          |    96.3 |    85.71 |     100 |    96.3 | 75-76,184,252-258 
 ...tools/workflow |   83.95 |    83.05 |   73.33 |   83.95 |                   
  workflow.ts      |   83.95 |    83.05 |   73.33 |   83.95 | ...11,456,458-459 
 src/utils         |   92.84 |    89.61 |   96.84 |   92.84 |                   
  LruCache.ts      |     100 |      100 |     100 |     100 |                   
  ...Controller.ts |     100 |      100 |     100 |     100 |                   
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...cFileWrite.ts |   94.94 |    92.47 |     100 |   94.94 | ...43-544,651-655 
  bareMode.ts      |   81.81 |      100 |      50 |   81.81 | 18-19             
  ...ry-content.ts |   98.45 |    95.45 |     100 |   98.45 | 132-133,159-160   
  browser.ts       |   86.84 |    78.94 |     100 |   86.84 | 34,36-37,65-66    
  btwUtils.ts      |   13.95 |      100 |       0 |   13.95 | 17-31,34-55       
  bundlePaths.ts   |     100 |      100 |     100 |     100 |                   
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  ...ncyLimiter.ts |   94.64 |    95.23 |     100 |   94.64 | 64-66             
  ...igResolver.ts |     100 |      100 |     100 |     100 |                   
  ...engthError.ts |   91.06 |    89.47 |     100 |   91.06 | ...46-147,154-155 
  ...n-branches.ts |   95.81 |    93.95 |      95 |   95.81 | ...91-492,504-517 
  ...tion-chain.ts |     100 |      100 |     100 |     100 |                   
  cronDisplay.ts   |     100 |    97.61 |     100 |     100 | 46                
  cronParser.ts    |   95.34 |    93.33 |     100 |   95.34 | 41-42,47-48,70-71 
  debugLogger.ts   |   96.66 |    96.61 |   88.88 |   96.66 | 192-196           
  editHelper.ts    |   93.63 |     83.9 |     100 |   93.63 | ...27-428,462-463 
  editor.ts        |   97.65 |    95.45 |     100 |   97.65 | ...35-336,338-339 
  encoding.ts      |     100 |      100 |     100 |     100 |                   
  env.ts           |     100 |      100 |     100 |     100 |                   
  ...arResolver.ts |   94.28 |    88.88 |     100 |   94.28 | 28-29,125-126     
  ...entContext.ts |   96.63 |    90.13 |   96.66 |   96.63 | ...42,444-445,512 
  errorParsing.ts  |     100 |      100 |     100 |     100 |                   
  ...rReporting.ts |   95.65 |    93.33 |     100 |   95.65 | 37-38             
  errors.ts        |   82.62 |    94.32 |    61.9 |   82.62 | ...62-378,382-388 
  fetch.ts         |   90.68 |    82.51 |     100 |   90.68 | ...72,483-484,503 
  fileUtils.ts     |   94.87 |    92.95 |   96.15 |   94.87 | ...1907,1915-1916 
  forkedAgent.ts   |   92.45 |    82.35 |   93.75 |   92.45 | ...34,642,647-654 
  formatters.ts    |     100 |      100 |     100 |     100 |                   
  ...eUtilities.ts |    92.4 |    86.95 |     100 |    92.4 | ...52-158,168-169 
  ...rStructure.ts |   94.39 |    94.28 |     100 |   94.39 | ...29-132,343-348 
  getPty.ts        |   31.57 |       50 |     100 |   31.57 | 26-38             
  git-branches.ts  |    91.6 |    84.21 |    92.3 |    91.6 | ...90,405-410,570 
  gitDiff.ts       |   95.19 |    81.36 |     100 |   95.19 | ...1073,1419-1420 
  gitDirect.ts     |   98.84 |    94.28 |     100 |   98.84 | 234,318           
  ...noreParser.ts |   94.48 |    93.22 |     100 |   94.48 | ...23-124,158-159 
  gitUtils.ts      |   78.02 |    81.25 |   85.71 |   78.02 | ...22-123,147-198 
  github-prs.ts    |   95.74 |    82.27 |     100 |   95.74 | 216,314-322       
  iconvHelper.ts   |     100 |      100 |     100 |     100 |                   
  ...rePatterns.ts |     100 |      100 |     100 |     100 |                   
  image-view.ts    |   95.12 |    93.33 |     100 |   95.12 | ...68-172,240-244 
  ...ionManager.ts |     100 |     90.9 |     100 |     100 | 27                
  ...lPromptIds.ts |     100 |      100 |     100 |     100 |                   
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  jsonl-utils.ts   |   95.27 |     93.1 |     100 |   95.27 | ...16-317,359-362 
  ...-detection.ts |     100 |      100 |     100 |     100 |                   
  ...iconv-lite.ts |     100 |      100 |     100 |     100 |                   
  ...simple-git.ts |   96.77 |    91.66 |     100 |   96.77 | 38                
  ...m-headless.ts |      96 |    88.88 |     100 |      96 | 34                
  ...iagnostics.ts |    96.4 |     94.2 |     100 |    96.4 | ...66,293-294,376 
  ...yDiscovery.ts |    92.4 |    89.13 |     100 |    92.4 | ...28,331,522-525 
  ...tProcessor.ts |   94.01 |       90 |     100 |   94.01 | ...47-353,445-446 
  ...Inspectors.ts |     100 |      100 |     100 |     100 |                   
  modelId.ts       |   98.96 |    98.21 |     100 |   98.96 | 153               
  ...kerChecker.ts |    90.9 |    91.66 |     100 |    90.9 | 73-79             
  notebook.ts      |   94.57 |    89.91 |   95.83 |   94.57 | ...21,333,385-387 
  openaiLogger.ts  |   91.66 |    89.74 |     100 |   91.66 | ...26-228,251-256 
  osc8.ts          |   54.26 |    64.86 |   83.33 |   54.26 | ...72-195,197-257 
  partUtils.ts     |     100 |    98.64 |     100 |     100 | 211               
  pathReader.ts    |     100 |      100 |     100 |     100 |                   
  paths.ts         |   93.99 |    92.85 |     100 |   93.99 | ...88-489,491-493 
  pdf.ts           |   92.17 |    85.81 |     100 |   92.17 | ...64-565,606-611 
  projectPath.ts   |     100 |      100 |     100 |     100 |                   
  projectRoot.ts   |   71.73 |    78.57 |     100 |   71.73 | 54-66             
  ...ectSummary.ts |   89.62 |    72.41 |     100 |   89.62 | ...40-145,196-199 
  ...tIdContext.ts |     100 |      100 |     100 |     100 |                   
  proxyUtils.ts    |     100 |      100 |     100 |     100 |                   
  ...rDetection.ts |   71.15 |       86 |     100 |   71.15 | ...-90,96-101,147 
  ...noreParser.ts |   92.63 |    91.66 |     100 |   92.63 | ...77-178,197-198 
  rateLimit.ts     |   93.75 |    89.62 |     100 |   93.75 | ...13,218-219,262 
  ...text-range.ts |    96.9 |    86.85 |     100 |    96.9 | ...59-660,735-736 
  readManyFiles.ts |   95.75 |    80.86 |     100 |   95.75 | ...05,558,568-572 
  retry.ts         |   96.09 |    92.52 |     100 |   96.09 | ...67,558-559,577 
  retryContext.ts  |     100 |      100 |     100 |     100 |                   
  ...sification.ts |   97.63 |    97.02 |     100 |   97.63 | ...17,251-252,278 
  retryPolicy.ts   |   97.72 |    90.56 |     100 |   97.72 | 130-131           
  ripgrepUtils.ts  |   90.04 |    93.43 |   95.45 |   90.04 | ...55-565,598-599 
  ...sDiscovery.ts |   97.46 |    93.05 |     100 |   97.46 | ...04,182-183,202 
  ...iagnostics.ts |   83.08 |     67.5 |   92.59 |   83.08 | ...23,543-544,550 
  ...tchOptions.ts |   84.87 |    86.61 |   96.29 |   84.87 | ...71,696,725-734 
  ...odelPrefix.ts |     100 |      100 |     100 |     100 |                   
  runtimeStatus.ts |    97.5 |    89.74 |     100 |    97.5 | 162-163           
  safe-mode.ts     |     100 |      100 |     100 |     100 |                   
  safeJsonParse.ts |     100 |      100 |     100 |     100 |                   
  ...nStringify.ts |     100 |      100 |     100 |     100 |                   
  ...-child-env.ts |     100 |      100 |     100 |     100 |                   
  ...aConverter.ts |   98.03 |    97.75 |     100 |   98.03 | 100,102-103       
  ...aValidator.ts |   92.09 |    83.65 |   90.47 |   92.09 | ...60,882-883,896 
  ...r-launcher.ts |   96.35 |    93.97 |   85.71 |   96.35 | ...35-336,347-348 
  sedEditParser.ts |   91.78 |    92.18 |     100 |   91.78 | ...66-569,645-646 
  ...nIdContext.ts |     100 |      100 |     100 |     100 |                   
  ...orageUtils.ts |   95.98 |    83.96 |     100 |   95.98 | ...70,386,466,485 
  ...-pager-env.ts |     100 |      100 |     100 |     100 |                   
  ...fety-rules.ts |     100 |     89.7 |     100 |     100 | ...01,304,309-311 
  shell-utils.ts   |   86.07 |    88.36 |     100 |   86.07 | ...2269,2276-2280 
  ...lAstParser.ts |   98.16 |    91.91 |     100 |   98.16 | ...1244-1246,1256 
  ...ContextEnv.ts |     100 |       92 |     100 |     100 | 50-52             
  ...nlyChecker.ts |   96.33 |    96.57 |     100 |   96.33 | ...83-284,292-293 
  sideQuery.ts     |   86.82 |    86.66 |     100 |   86.82 | ...79-185,187-193 
  ...pEventSink.ts |     100 |       80 |     100 |     100 | 61                
  ...tGenerator.ts |     100 |      100 |     100 |     100 |                   
  ...ameContext.ts |     100 |      100 |     100 |     100 |                   
  symlink.ts       |   77.77 |       50 |     100 |   77.77 | 44,54-59          
  ...e-encoding.ts |   85.96 |    76.47 |     100 |   85.96 | 58-61,64-65,78-79 
  ...emEncoding.ts |   96.36 |    91.17 |     100 |   96.36 | 59-60,124-125     
  terminalSafe.ts  |     100 |      100 |     100 |     100 |                   
  ...Serializer.ts |   98.72 |       90 |     100 |   98.72 | 42-43,134,201-203 
  testUtils.ts     |   53.33 |      100 |   33.33 |   53.33 | ...53,59-64,70-72 
  ...-constants.ts |     100 |      100 |     100 |     100 |                   
  textUtils.ts     |      65 |      100 |      75 |      65 | 56-75             
  thoughtUtils.ts  |     100 |    95.65 |     100 |     100 | 99                
  ...-converter.ts |   95.23 |    85.71 |     100 |   95.23 | 36-37             
  ...name-utils.ts |     100 |      100 |     100 |     100 |                   
  ...-finalizer.ts |   97.66 |     90.9 |     100 |   97.66 | 165-166,168-172   
  tool-utils.ts    |    95.2 |    93.61 |     100 |    95.2 | ...58-159,162-163 
  ...ultCleanup.ts |   54.62 |       64 |      75 |   54.62 | ...03-105,108-134 
  ...Compaction.ts |   96.13 |    96.42 |     100 |   96.13 | ...34-339,341-346 
  ...pt-records.ts |   85.85 |    83.63 |     100 |   85.85 | ...90-394,424-439 
  truncation.ts    |   90.56 |    90.43 |     100 |   90.56 | ...35-443,480-486 
  windowsPath.ts   |   89.47 |    79.31 |     100 |   89.47 | ...57-58,62,90-91 
  ...aceContext.ts |   95.81 |    89.39 |     100 |   95.81 | ...74-275,299-301 
  xml.ts           |    97.8 |    87.69 |     100 |    97.8 | 98-99             
  yaml-parser.ts   |   83.87 |    77.27 |     100 |   83.87 | ...31-234,239-240 
 ...ils/filesearch |   83.68 |    80.38 |   94.69 |   83.68 |                   
  crawlCache.ts    |     100 |      100 |     100 |     100 |                   
  crawler.ts       |   82.47 |    76.22 |      95 |   82.47 | ...1525,1559-1560 
  fileSearch.ts    |   93.78 |    87.67 |     100 |   93.78 | ...71-272,274-275 
  fzfWorker.ts     |       0 |        0 |       0 |       0 | 1-109             
  ...rkerHandle.ts |   84.05 |    75.43 |   89.47 |   84.05 | ...30-334,340-341 
  ignore.ts        |     100 |    97.36 |     100 |     100 | 187               
  result-cache.ts  |     100 |    93.75 |     100 |     100 | 49                
 ...uest-tokenizer |   69.76 |    75.47 |   85.29 |   69.76 |                   
  ...eTokenizer.ts |   65.72 |    74.02 |    92.3 |   65.72 | ...65-466,479-533 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tTokenizer.ts |   68.39 |    69.49 |    90.9 |   68.39 | ...24-325,327-328 
  ...ageFormats.ts |   76.92 |      100 |   33.33 |   76.92 | 46-49,56-57       
  textTokenizer.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 | 1                 
-------------------|---------|----------|---------|---------|-------------------

For detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run.

@wenshao

wenshao commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@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-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Jul 31, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

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

@wenshao

wenshao commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Review — built and exercised locally on Linux

I checked this branch out into an isolated worktree, ran its own suite, and then drove the three new capabilities directly against real git repos rather than reading them. Everything below that says "measured" was produced by running the code at c0481dd.

PR head c0481dd
Base a182bdf (merge-base with origin/main)
Suite npx vitest run src/commands/review src/commands/review.test.ts42 files, 1161 tests passed

The Test Plan's own headline claim reproduces exactly — 42 files / 1161 tests, the numbers in the description. The design is right: the determinism/judgment split is applied consistently, the non-capping contract for testPlanGate is well tested from five angles, base-tree.test.ts exercising real git worktree lifecycle (stale sibling, sibling-not-nested, build-failed-so-unavailable) is the right call, and the integration test pinning the gating bug — hunk probes running only when a mutant already existed — is exactly the regression that would otherwise silently un-ship the feature. Path handling in test-plan is careful: .. normalises to unchecked, PATH_RE never admits a leading /, and both halves of a note go through mdField, so an untrusted PR body cannot inject markdown into the posted review.

Eight things below. One I'd like fixed before merge.


🔴 1. BOLD_LINE_RE backtracks catastrophically on an untrusted PR body — test-plan.ts:119

const BOLD_LINE_RE = /^\*\*\s*([^*\n]+?)\s*\*\*:?\s*$/;

\s*, [^*\n]+? and \s* all match a space, so a line that starts ** and never closes it makes the engine walk every split of a whitespace run. extractTestPlanSection runs this unconditionally on every non-fenced line of the body (line 151), and the body comes from gh pr view on the PR under review — a fork contributor's prose. A fork PR into this repo still matches a local remote, so it is reviewed in worktree mode and test-plan runs on it; lightweight mode is not the escape hatch here.

Measured, single exec call:

line time
**Test Plan** 0.0 ms
** + 2 000 spaces 0.96 s
** + 3 000 spaces 3.2 s
** + 5 000 spaces 14.4 s
** + 20 000 spaces > 100 s (killed)

GitHub allows a 65 536-character body, and the scan is per line, so this is unbounded in practice. Ordinary prose is safe (single spaces between words measured 0.0 ms at 10 kB) — this is a crafted-input hazard, not an accidental one, which is exactly the input class the command reads.

Removing the ambiguity fixes it with no behaviour change:

const BOLD_LINE_RE = /^\*\*([^*\n]+)\*\*:?\s*$/;
// …then .trim() the capture at the two use sites

Verified equivalent on every real input (**Test Plan**, ** Test Plan **, **Test Plan**:, **Reviewer Test Plan** , **测试计划**, not bold, **a**b — all identical), and 0.1 ms on ** + 60 000 spaces.

🟠 2. A hunk probe throws ENOENT when the hunk adds a file in a new directory — test-efficacy.ts:1182

git apply --reverse on a new file mode hunk deletes the file and removes the parent directories it emptied. The restore in the finally then writes into a directory that no longer exists:

} finally {
  writeFileSync(abs, original, 'utf8');   // ENOENT
}

Measured against the real runOneHunkProbe:

added file in a brand-new dir   → Error: ENOENT: no such file or directory,
                                  open '/tmp/audit-hunk-Ptze7y/pkg/newdir/f.ts'
added file in an EXISTING dir   → clean restore (control)

Because it throws from finally, the verdict is lost and the exception unwinds into the catch at line 1519, which marks that hunk and every remaining one inconclusive. So a PR that adds a file in a directory it also creates gets no per-hunk signal at all — the accounting stays honest, but the capability silently no-ops on a common diff shape. The doc comment three lines up ("Writing the saved bytes back also recreates a file the reverse patch deleted") is true only when the directory survives, and that case is the one with no test.

mkdirSync(dirname(abs), { recursive: true }) before the write covers it.

🟠 3. base-tree has no lease, and Step 4's verifier shards run in parallel — base-tree.ts:140

Step 4 launches ceil(N/8) verifiers "together in one response", and the brief now offers every one of them qwen review base-tree. They all resolve the same path (baseWorktreePath(worktree)), and the first thing the command does is destroy whatever is there:

sweep = discardWorktree(worktree, tree);   // git worktree remove --force + rmSync -rf
git(worktree, 'worktree', 'add', '--detach', tree, baseSha);

Two shards that both hit a comparative claim will clobber each other. The benign interleaving is a build that dies with confusing ENOENTs. The one that worries me: shard A finishes the build, gets available: true, shard B sweeps the tree, and A's A/B command then produces empty output from the BASE side — which reads as "the PR changed this behaviour" and gets quoted with Source: [probe], which compose-review treats as deterministic. A fabricated A/B is worse than no A/B.

The probe tree never had this problem because only Agent 7 ever creates it; base-tree is the first disposable sibling handed to N concurrent agents. "One extra build per review, at most" is an instruction to a model, not an invariant two shards can honour. An idempotent fast path would fix the race and the duplicate cost at once: if the tree exists, is registered, its HEAD equals baseSha, and a marker records a successful build, return available: true without touching it.

🟠 4. npmScriptOf reads a leading workspace flag as the script name — test-plan.ts:368

const m = /^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?([\w:.-]+)/.exec(command);

- is in the character class, so when the flag precedes the script the flag is the capture. End-to-end through runTestPlantestPlanGate, against a manifest that defines build and test:

`npm --workspace=packages/cli run build` — `no package defines this script`
`npm -w packages/cli run test`           — `no package defines this script`
`npm run build`                          — (no note)          ← control

Those two lines get posted to the author, on a Test Plan that is correct. npm run test --workspace=x (flag after) is fine, which is why the existing test passes — but npmScriptOf's test is titled "reads the script name past run and past a workspace flag" and its workspace case is npm test --workspace=packages/cli, where nothing is actually being stepped over. The forms that break are the ones the test name claims to cover. yarn --cwd packages/cli build has the same shape.

Given the design goal is "biased toward silence", skipping a capture that starts with - (or walking flag tokens to the first non-flag) seems worth it.

🟡 5. hunk-survived re-states what the revert probe already said

When the whole-file revert probe comes back inert, every hunk in that file is going to survive too — reverting one change cannot break a suite that survived reverting all of them. The PR's own integration run shows both findings from a one-file diff:

Wrote test-efficacy report … (0 unreachable, 1 probed, 0 mutant(s), 1 hunk probe(s), 2 finding(s))
  [test] inert: packages/lib/src/f.test.ts
  [test] hunk-survived: packages/lib/src/f.ts

Scale that to a real un-gated file and it is 1 inert + up to 6 hunk-survived findings saying the same thing, bought with up to six full suite runs. The mutant-overlap skip already encodes the "don't pay twice for the same answer" rule; this is the same rule one level up. It is awkward to gate on directly since the revert probe runs after the mutation phase — but it is worth either reordering or noting the redundancy in the finding text.

🟡 6. A red baseline is reported to the reader as a budget skip — test-efficacy.ts:1483

Mutants get a dedicated skippedForBaseline; hunks reuse skippedForBudget:

if (greenProbes.length === 0) {
  mutantsSkippedForBaseline = candidates.length;
  hunksSkippedForBudget = hunkCandidates.length;

which then prints (line 1691):

  N hunk probe(s) skipped: the mutants used the window

The mutants ran zero suites in that branch. The count is honest, the reason is not, and the stacked note only mentions mutants. A skippedForBaseline field on hunks would mirror the mutants and cost nothing.

🟡 7. splitDiffIntoHunks gives every hunk the first file's header — test-efficacy.ts:1031

fileHeader is captured once, before the first @@, and reused for every hunk — but the scan explicitly stops a hunk body at diff --git , i.e. it is written as if multi-file input were expected. Measured on a two-file diff:

hunks[1].patch → "diff --git a/one.ts b/one.ts … -const b = 1; +const b = 2;"

A patch pointing at the wrong file. Unreachable today (hunkProbeInputs diffs one path at a time, so this is latent), and the most likely outcome would be a rejected patch → inconclusive rather than a wrong verdict. But it is exported and unit-tested standalone, so it reads as general-purpose. Either reset fileHeader when a diff --git line is crossed, or return [] on multi-file input.

🟡 8. base-tree costs a second npm ci, not "one extra build"

The base worktree is a cold checkout: package-lock.json present, node_modules/.package-lock.json absent, so runBuildTest's install gate fires and runs a full npm ci — the one the file itself measures at ~1.4 G of tree behind a 3 GiB free-space preflight (INSTALL_MIN_FREE_BYTES). DESIGN.md, SKILL.md and the verifier brief all price this as "a second build is a second build" / "one extra build".

Worth noting because test-efficacy documents the opposite reasoning for its own sibling: "node_modules resolves without a per-tree install because the probe tree is nested under the repo … so Node walks up to the repo-root node_modules". The base tree is nested the same way. If that reasoning holds for the probe tree it likely holds here too, and install: false would make the cost claim true. If it does not hold — because the build needs .bin binaries the probe tree never invokes — then the docs should say "an install and a build", since that is what a reviewer is agreeing to spend.


Nothing here is about the design, which I think is right — the base tree in particular closes a gap the pipeline has had since it existed. Fixing 1 is what I'd hold the merge on; 2–4 all produce wrong or missing output on ordinary inputs and are each a few lines; 5–8 are notes.

中文说明

评审 —— 在 Linux 本地构建并实测

把该分支检出到隔离 worktree,跑了它自己的套件,然后针对真实 git 仓库直接驱动三个新能力,而不是靠读代码判断。下文凡标注"实测"的结论,均由 c0481dd 处的代码实际运行产生。

PR head c0481dd
Base a182bdf(与 origin/main 的 merge-base)
套件 npx vitest run src/commands/review src/commands/review.test.ts42 个文件、1161 项测试全部通过

Test Plan 的核心断言逐字复现 —— 42 个文件 / 1161 项测试,与描述中的数字一致。设计方向是对的:确定性/判断力的拆分贯彻一致;testPlanGate 的"不阻断、不封顶"契约从五个角度做了测试;base-tree.test.ts 用真实 git 覆盖 worktree 生命周期(残留 sibling、必须并列而非嵌套、构建失败即不可用)是正确取舍;而那条钉住 gating bug(hunk 探针曾只在已有 mutant 时才运行)的集成测试,恰好是唯一能防止该特性被静默废掉的回归。test-plan 的路径处理很谨慎:.. 归为 uncheckedPATH_RE 不接受前导 /,note 的两半都经过 mdField,因此不可信的 PR 正文无法向评审正文注入 markdown。

以下 8 条。第 1 条希望合并前修复。


🔴 1. BOLD_LINE_RE 在不可信 PR 正文上发生灾难性回溯 —— test-plan.ts:119

const BOLD_LINE_RE = /^\*\*\s*([^*\n]+?)\s*\*\*:?\s*$/;

\s*[^*\n]+?\s* 三者都能匹配空格,因此一行以 ** 开头且永不闭合时,引擎会穷举空白串的每一种切分。extractTestPlanSection 对正文中每一个非 fence 行无条件执行该正则(第 151 行),而正文来自对被审 PR 的 gh pr view —— 也就是 fork 贡献者写的文字。fork PR 提交到本仓库时仍能匹配本地 remote,因此走 worktree 模式test-plan 会运行;lightweight 模式在这里不构成豁免。

单次 exec 实测:

输入 耗时
**Test Plan** 0.0 ms
** + 2 000 空格 0.96 s
** + 3 000 空格 3.2 s
** + 5 000 空格 14.4 s
** + 20 000 空格 > 100 s(被杀)

GitHub 允许 65 536 字符的正文,且扫描是逐行的,实际上不设上限。普通散文是安全的(10 kB 单空格分词文本实测 0.0 ms)—— 这是构造输入型风险,而非偶发风险,而构造输入恰好就是该命令读取的那一类输入。

消除歧义即可修复,行为不变:

const BOLD_LINE_RE = /^\*\*([^*\n]+)\*\*:?\s*$/;
// 然后在两个使用点对捕获组 .trim()

已验证在全部真实输入上完全等价(**Test Plan**** Test Plan ****Test Plan**:**Reviewer Test Plan** **测试计划**not bold**a**b —— 结果全部一致),且 ** + 60 000 空格仅需 0.1 ms

🟠 2. 当 hunk 是"在新建目录中新增文件"时,探针恢复抛 ENOENT —— test-efficacy.ts:1182

git apply --reverse 作用于 new file mode 的 hunk 时,会删除该文件并移除因此变空的父目录finally 里的恢复随后写入一个已不存在的目录:

} finally {
  writeFileSync(abs, original, 'utf8');   // ENOENT
}

针对真实 runOneHunkProbe 实测:

新增文件位于全新目录  → Error: ENOENT: no such file or directory,
                        open '/tmp/audit-hunk-Ptze7y/pkg/newdir/f.ts'
新增文件位于已有目录  → 恢复正常(对照组)

由于异常抛自 finally,该 hunk 的裁定丢失,异常上抛到第 1519 行的 catch,把它以及后续全部 hunk标记为 inconclusive。于是"新增了自己创建的目录下的文件"这一常见 diff 形态,完全拿不到 per-hunk 信号 —— 账目仍然诚实,但能力静默失效。上方三行的注释("写回保存的字节也会重建被反向 patch 删除的文件")只在目录幸存时成立,而不成立的那种情况恰好没有测试。

在写回前加 mkdirSync(dirname(abs), { recursive: true }) 即可。

🟠 3. base-tree 没有租约,而 Step 4 的 verifier 分片是并行的 —— base-tree.ts:140

Step 4 会在一次响应中同时启动 ceil(N/8) 个 verifier,而新的 brief 向它们每一个都提供了 qwen review base-tree。它们解析出的是同一个路径(baseWorktreePath(worktree)),而命令做的第一件事就是摧毁该路径上的一切:

sweep = discardWorktree(worktree, tree);   // git worktree remove --force + rmSync -rf
git(worktree, 'worktree', 'add', '--detach', tree, baseSha);

两个分片若都遇到比较型断言,就会互相清除。良性交错是构建以莫名其妙的 ENOENT 失败。真正让我担心的是:分片 A 构建完成、拿到 available: true,分片 B 清除了该树,随后 A 的 A/B 命令在 BASE 侧得到空输出 —— 读起来就是"该 PR 改变了这个行为",并带着 Source: [probe] 被引用,而 compose-review 把它当作确定性证据。伪造的 A/B 比没有 A/B 更糟。

probe 树从未有此问题,因为只有 Agent 7 会创建它;base-tree 是第一个交给 N 个并发 agent 的可弃 sibling。"每次评审最多一次额外构建"是给模型的指令,不是两个分片能共同遵守的不变量。一个幂等快路径可以同时解决竞争和重复成本:若该树存在、已注册、HEAD 等于 baseSha、且标记文件记录构建成功,则直接返回 available: true,不做任何改动。

🟠 4. npmScriptOf 把前置的 workspace flag 当成脚本名 —— test-plan.ts:368

const m = /^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?([\w:.-]+)/.exec(command);

- 在字符类里,所以当 flag 位于脚本名之前时,捕获到的就是 flag 本身。经 runTestPlantestPlanGate 端到端实测(manifest 定义了 buildtest):

`npm --workspace=packages/cli run build` — `no package defines this script`
`npm -w packages/cli run test`           — `no package defines this script`
`npm run build`                          — (无 note)        ← 对照组

这两行会被发到作者面前,而作者的 Test Plan 完全正确。npm run test --workspace=x(flag 在后)没问题,这也是现有测试能过的原因 —— 但 npmScriptOf 的用例标题是 "reads the script name past run and past a workspace flag",其 workspace 用例却是 npm test --workspace=packages/cli,实际并没有跨过任何东西。真正出问题的形态,恰好就是标题声称覆盖的那些。yarn --cwd packages/cli build 同形。

既然设计目标是"偏向沉默",跳过以 - 开头的捕获(或遍历 flag token 直到第一个非 flag)应该是值得的。

🟡 5. hunk-survived 在重复 revert 探针已经说过的话

当整文件 revert 探针返回 inert 时,该文件的每个 hunk 必然也 survive —— 全部改动回退都没打破的套件,不可能被回退单个改动打破。该 PR 自己的集成运行就在一个单文件 diff 上同时产出了两条 finding:

Wrote test-efficacy report … (0 unreachable, 1 probed, 0 mutant(s), 1 hunk probe(s), 2 finding(s))
  [test] inert: packages/lib/src/f.test.ts
  [test] hunk-survived: packages/lib/src/f.ts

放大到真实的无守护文件,就是 1 条 inert + 最多 6 条说着同一件事的 hunk-survived,代价是最多 6 次完整套件运行。mutant 重叠跳过规则已经写下了"同一个答案不付两次钱";这是同一条规则的上一层。直接 gating 确实别扭(revert 探针在 mutation 阶段之后才跑)—— 但要么调整顺序,要么在 finding 文案里点明这层冗余,都是值得的。

🟡 6. 基线为红时,向读者报告成了预算跳过 —— test-efficacy.ts:1483

mutant 有专门的 skippedForBaseline,hunk 却复用 skippedForBudget

if (greenProbes.length === 0) {
  mutantsSkippedForBaseline = candidates.length;
  hunksSkippedForBudget = hunkCandidates.length;

随后打印(第 1691 行):

  N hunk probe(s) skipped: the mutants used the window

而该分支里 mutant 一次套件都没跑。数字是诚实的,理由不是,且堆叠的 note 只提到 mutant。给 hunks 加一个 skippedForBaseline 字段与 mutant 对齐,成本为零。

🟡 7. splitDiffIntoHunks 给每个 hunk 都套上第一个文件的头 —— test-efficacy.ts:1031

fileHeader 在第一个 @@ 之前只捕获一次,之后被所有 hunk 复用 —— 但扫描又显式地在 diff --git 处结束 hunk 体,也就是说它是按"可能收到多文件输入"来写的。双文件 diff 实测:

hunks[1].patch → "diff --git a/one.ts b/one.ts … -const b = 1; +const b = 2;"

一个指向错误文件的 patch。目前不可达(hunkProbeInputs 每次只 diff 一个路径,因此这是潜在缺陷),且最可能的后果是 patch 被拒 → inconclusive,而非错误裁定。但它是导出的、并有独立单测,读起来像通用工具。要么在跨越 diff --git 行时重置 fileHeader,要么对多文件输入直接返回 []

🟡 8. base-tree 的代价是第二次 npm ci,而不是"一次额外构建"

base worktree 是冷检出:package-lock.json 在、node_modules/.package-lock.json 不在,于是 runBuildTest 的安装门槛触发,执行完整 npm ci —— 也就是该文件自己测量过的、约 1.4 G 依赖树、且带 3 GiB 空间预检(INSTALL_MIN_FREE_BYTES)的那次安装。DESIGN.md、SKILL.md 和 verifier brief 都把它计价为"第二次构建就是第二次构建" / "一次额外构建"。

之所以值得一提,是因为 test-efficacy 对自己的 sibling 给出的是相反论证:"node_modules 无需按树安装即可解析,因为 probe 树嵌套在仓库内 …… Node 会向上走到仓库根的 node_modules"。base 树的嵌套方式完全相同。若该论证对 probe 树成立,这里多半也成立,install: false 能让成本声明变成真的。若不成立 —— 比如构建需要 probe 树从不调用的 .bin 可执行文件 —— 那文档应改成"一次安装加一次构建",因为这才是 reviewer 实际同意支付的东西。


这些都不针对设计本身,我认为设计是对的 —— 尤其 base tree 填上了流水线自诞生起就有的缺口。第 1 条是我会卡合并的那条;第 2–4 条在普通输入上就会产出错误或缺失的输出,且各自只需几行;第 5–8 条是备注。

Reviewed with Claude Code (Opus 5, 1M context)

@qwen-code-dev-bot

qwen-code-dev-bot commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

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

中文说明

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

All measured in the review (#8215 review comment):

- test-plan: linear-time bold-heading scan (the old pattern backtracked
  catastrophically on an unclosed ** line an untrusted PR body controls);
  a flag preceding the npm script yields no claim instead of a false
  'no package defines this script'.
- test-efficacy: a hunk probe's restore recreates the parent directory a
  reverse-applied 'new file' hunk removed (the ENOENT from finally lost
  the verdict and marked every remaining hunk inconclusive); hunks get
  their own skippedForBaseline instead of mislabeling a red baseline as
  a budget skip; splitDiffIntoHunks re-captures the file header at every
  diff --git boundary; a hunk-survived finding notes when it restates an
  inert file-level revert at hunk granularity.
- base-tree: idempotent fast path keyed on a build marker + HEAD check —
  concurrent verifier shards reuse one built tree instead of sweeping it
  out from under each other mid-A/B (a fabricated base-side difference
  with a deterministic source tag was the worst case); cost wording is
  now 'an install and a build' everywhere it was 'one extra build'.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@qwen-code-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 将重新运行。

@wenshao

wenshao commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Review

Three genuinely new capabilities, each with a clear reason to exist and a stated refusal built into it (differs never becomes a contradiction, an unavailable base is never a finding, a compile failure is never killed). The code matches the design docs, the test suites are unusually good at pinning the negative cases, and the two supporting fixes (ANSI stripping, summary rescue) are clearly field-derived. No Criticals. Everything below is a Suggestion or lower.

Verified

  • cd packages/cli && npx vitest run src/commands/review src/commands/review.test.ts at head 67ac8aa (isolated worktree, npm ci): 42 files / 1167 tests pass. (Test Plan says 1161 — differs, in this PR's own vocabulary. 🙂)
  • eslint and prettier --check clean on all 20 changed files.
  • Findings 1–3 and 6 below were each reproduced by driving the exported functions directly; verdicts quoted are observed output, not readings.

1. test-plan lifts diff/log paths out of pasted evidence and posts them as contradicted (Suggestion)

extractClaims scans every fenced block regardless of its info string, and this repo's own template nests ### Evidence (Before & After) inside ## Reviewer Test Plan — precisely where authors paste diffs and runner output.

Observed, feeding a Test Plan whose Evidence block contains a normal unified diff:

claims:   [{"kind":"path","text":"a/packages/cli/src/x.ts"},{"kind":"path","text":"b/packages/cli/src/x.ts"}]
verdicts: ["a/packages/cli/src/x.ts","contradicted","no such file or directory"]
          ["b/packages/cli/src/x.ts","contradicted","no such file or directory"]

testPlanGate turns both into body notes, so an otherwise-clean Approve carries Test Plan (not a blocker): `a/packages/cli/src/x.ts` — `no such file or directory`; … — a factually wrong public statement, which is the exact cost DESIGN.md says the differs ruling exists to avoid. A pasted FAIL packages/cli/src/ghost.test.ts line does the same thing.

Suggested: skip fenced blocks whose info string is an output language (diff, text, log, console, json), drop tokens carrying the a/ / b/ diff prefix, and strip <!-- … --> HTML comments (leftover template instructions) before extraction. All three keep the "biased toward silence" contract.

2. The base-tree reuse marker only guards the window after a successful build (Suggestion)

The marker is written after build.ok, so the fast path covers the short window and leaves the long one open. discardWorktree then runs unconditionally. Two consequences, both reproduced:

(a) A shard arriving mid-build wipes the tree under the shard that is building it. Shard B's opening sweep deleted shard A's build output while A was still building; A then reported:

available: false
note: "the base tree at 0acb567f0 did not build, so nothing can be run against it …"

That is a false statement about the base tree, disclosed to the reviewer as a harness fact. The code comment says "the window is narrow" — it is actually the whole npm ci + build, i.e. the longest window in the command and the one concurrent shards are most likely to land in.

(b) A base that does not build is re-swept and re-built by every shard. No marker is written on failure, so three sequential calls produced three full install+build attempts. agent-briefs.ts promises verifiers "An install and a build, once per review at most"; that does not hold on the path where the base is broken — which is also the slowest path.

Suggested: claim the tree before building — writeFileSync(marker, sha, { flag: 'wx' }) or a lock dir — and have a caller that finds a fresh in-progress claim report "another verifier is building it" instead of sweeping; record the failed build in the marker so repeat callers reuse the negative result. (This also closes a smaller hole: the post-build writeFileSync(marker()) can throw ENOENT straight out of runBaseTree if the tree was swept in between — the only uncaught throw on that path.)

3. trimOutput's new rescue predicate is unbounded (Suggestion)

RUNNER_SUMMARY_RE = /^\s*(?:Tests?|Test Files):?\s/ also matches jest's per-suite Test suite failed to run, which is emitted once per failing suite — exactly the situation the rescue was added for. Observed on a log with 400 such lines:

input 23,396 chars -> trimmed 19,980 chars   (KEEP_HEAD + KEEP_TAIL = 8,000)

The docstring's "Report stays bounded; the signal survives" no longer holds, and this output is fed to a model. Suggested: anchor on the summary shape (/^\s*(?:Tests|Test Files):?\s+\d/) and cap the rescued set (last ~20 lines), which also bounds the pre-existing MODULE_ERROR_RE path.

4. Hunk-selection failure is the one silent zero in the new probe (Suggestion)

Mutants disclose a selection failure through noteMutants(...) and carry mutants.note. The hunk equivalent is:

} catch {
  hunkCandidates = [];
}

no note — and the hunks report object has no note field at all. The report then reads probed: [], survived: 0, skippedForCap/Budget/Baseline: 0, which is indistinguishable from "every hunk came back clean". That is the exact false assurance DESIGN.md says must never be readable ("a hunk probe that never ran must never be readable as a hunk that came back clean"). Suggested: add hunks.note and a noteHunks(...) on that catch.

5. probeCreateFailureDetail is now a duplicate of worktreeCreateFailureDetail('probe', …) (Nice to have)

lib/worktree.ts exists so the step lives "here rather than twice", but only discardWorktree moved; probeCreateFailureDetail (test-efficacy.ts:907) stayed behind byte-identical modulo the label. Deleting it in favour of worktreeCreateFailureDetail('probe', …) finishes the extraction (its tests at test-efficacy.test.ts:167 move with it).

6. splitDiffIntoHunks leaks the first file's header when that file has no hunks (Nice to have — latent)

fileHeader = lines.slice(0, first) spans file boundaries, so a rename-only / binary / mode-change first entry is prepended to the next file's patch:

patch[0] = "diff --git a/old.ts b/new.ts\nsimilarity index 100%\nrename from old.ts\nrename to new.ts\ndiff --git a/x.ts b/x.ts\n--- a/x.ts\n+++ b/x.ts\n@@ -1 +1 @@\n-a\n+b\n"

Reverse-applying that undoes a rename the verdict is then attributed to. Not reachable through hunkProbeInputs today (it diffs one path at a time), but the function is exported, its own comment says it "reads as general-purpose, so it behaves as one", and its test file already covers multi-file diffs. One-line fix: when a diff --git precedes the first @@, start the scan there instead of slicing to first.

7. Smaller notes (Nice to have)

  • testPlanGate joins notes with '; ' and no cap — combined with pre-release: fix ci #1 a long Evidence block can produce a very long single body clause. Worth an N-and-"+K more" cap, as elsewhere.
  • base-tree.test.ts: expect(r.path!.startsWith(\${worktree}/`)).toBe(false)hardcodes/, so on Windows the assertion is vacuous. sep(orjoin`) keeps it meaningful on the platforms the Tested-on table marks ⚠️.
  • A rename+modify file arrives from git diff … -- <newpath> as new file with one whole-file hunk, so it consumes a cap slot and a full suite run to reproduce the all-or-nothing granularity the feature exists to escape. Cheap guard if it shows up in practice.
  • observedTestCounts misses Tests 1 failed | 2 skipped | 40 passed (a skipped segment between failed and passed). Fails silent to unchecked, so it's a coverage gap rather than a wrong ruling.
中文说明

三个能力各自有清晰的存在理由,并且都内建了明确的"拒绝"(differs 绝不升级为矛盾、base 不可用绝不成为 finding、编译失败绝不判 killed)。代码与设计文档一致,测试对反例的覆盖尤其扎实。无 Critical,以下均为 Suggestion 及以下。

已验证:head 67ac8aa 隔离 worktree + npm ci 下,npx vitest run src/commands/review src/commands/review.test.ts42 文件 / 1167 测试全通过(PR 描述写的是 1161,按本 PR 自己的词汇是 differs 🙂);20 个改动文件 eslintprettier --check 全清。findings 1–3、6 均通过直接驱动导出函数复现,引用的是实测输出。

  1. test-plan 会把粘贴在 Evidence 里的 diff/日志路径判成 contradicted(Suggestion)。extractClaims 不看 fence 的 info string,而本仓库模板把 ### Evidence (Before & After) 嵌在 ## Reviewer Test Plan 之内。实测一段普通 unified diff 产生 a/packages/cli/src/x.tsb/packages/cli/src/x.ts 两条 contradicted,会被 testPlanGate 写进 Approve 的正文——正是 DESIGN.mddiffers 规避的那种"错误评论"代价。建议:跳过输出型 fence(diff/text/log/console/json)、丢弃带 a/ b/ 前缀的 token、抽取前剥离 <!-- --> 注释。
  2. base-tree 的复用 marker 只守住了"构建成功之后"的窗口(Suggestion)。(a) 另一个 shard 在构建中途到达会 sweep 掉正在构建的树,受害 shard 报 available:false … did not build——对 base 树的错误陈述;代码注释说"窗口很窄",实际上那是整个 npm ci+build,是最长的窗口。(b) base 构建失败时不写 marker,实测连续三次调用 = 三次完整 install+build,与 brief 承诺的"每次 review 至多一次 install+build"不符。建议:构建flag:'wx' 或锁目录抢占,遇到新鲜的 in-progress 声明就报"另一个 verifier 正在构建"而不是 sweep;失败结果也记进 marker 以便复用。(顺带修掉构建后 writeFileSync(marker()) 在树被 sweep 时抛 ENOENT 直接逃出 runBaseTree 的问题。)
  3. trimOutput 新的抢救谓词无界(Suggestion)。/^\s*(?:Tests?|Test Files):?\s/ 会命中 jest 每个失败套件的 Test suite failed to run。实测 23,396 字符的日志裁剪后仍有 19,980 字符(预算 8,000),"报告保持有界"的承诺不再成立,而这段文本是喂给模型的。建议锚定汇总行形状并给抢救行数封顶。
  4. hunk 选择失败是新探针里唯一的静默零(Suggestion)。mutants 有 noteMutantsmutants.note,hunk 的 catch 只是 hunkCandidates = [],且 hunks 报告根本没有 note 字段;结果读起来与"每个 hunk 都干净"无法区分——正是 DESIGN.md 明令禁止的读法。
  5. probeCreateFailureDetail 现在与 worktreeCreateFailureDetail('probe', …) 完全重复(Nice to have)。lib/worktree.ts 的立意是"不要写两遍",但只搬走了 discardWorktree
  6. splitDiffIntoHunks 在首个文件没有 hunk 时会泄漏它的文件头(Nice to have,当前潜伏)。lines.slice(0, first) 跨越了文件边界,rename-only/二进制/模式变更的首条会被拼进下一个文件的 patch,反向应用时连 rename 一起撤销。当前调用点逐文件 diff 所以不可达,但该函数已导出且注释自称通用、测试也覆盖多文件 diff。
  7. 其它小项:testPlanGatejoin('; ') 无上限;base-tree.test.tsstartsWith(\${worktree}/`)硬编码/,在 Windows 上该断言恒真;rename+modify 文件会以整文件单 hunk 的形式吃掉一个 cap 名额和一次完整套件运行;observedTestCounts漏掉failed | skipped | passed这一形态(静默降级为unchecked`,不会误判)。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

No new change — all eight findings are already resolved on this branch

Thank you for the thorough, measured review. Every finding in it is already
addressed by commit 5054121b0 ("fix(review): address the eight findings from
live review of this branch"), which postdates the reviewed head c0481dd and is
already part of this PR's branch. No further code change is needed this round;
each item below was re-verified against the current HEAD rather than taken on
trust from the commit message.

# Finding Resolution on this branch
🔴 1 BOLD_LINE_RE catastrophic backtracking test-plan.ts: pattern is now /^\*\*([^*\n]+)\*\*:?\s*$/ — no ambiguous \s* and no lazy quantifier, so the scan is linear-time; the capture is trimmed at the use site (bold?.[1]?.trim()). Regression test: test-plan.test.ts "scans a hostile unclosed-bold line in linear time" (** + 50,000 spaces → null; a regression hangs into the timeout rather than failing an assertion).
🟠 2 Hunk probe ENOENT when a hunk adds a file in a new directory test-efficacy.ts: the finally restore now runs mkdirSync(dirname(abs), { recursive: true }) before writeFileSync, so the verdict is no longer lost and remaining hunks are no longer marked inconclusive. Regression test: test-efficacy.integration.test.ts "restores a hunk-ADDED file whose parent directory the reverse apply removed" (file under src/newdir/ is restored intact against real git).
🟠 3 base-tree has no lease while Step 4 shards run in parallel base-tree.ts: an idempotent fast path keyed on a .qwen-review-base-ok marker (written only after a successful build) plus a HEAD == baseSha check returns an already-built tree as-is, without sweeping it. Concurrent shards reuse one built tree, so the worst case — shard B sweeping the tree shard A is mid-A/B in, producing a fabricated base-side difference with a deterministic source tag — no longer occurs. A marker for a different SHA (a rebase between runs) falls through to a rebuild. Regression test: base-tree.test.ts "REUSES an already-built base tree instead of sweeping it" (two calls → one build; wrong-sha marker does not shortcut).
🟠 4 npmScriptOf reads a leading workspace flag as the script name test-plan.ts: if (name.startsWith('-')) return null; — a flag preceding the script yields unchecked ("biased toward silence"), never a false no package defines this script on a correct Test Plan. Regression test: test-plan.test.ts "is null when a FLAG precedes the script" (npm --workspace=packages/cli run build, npm -w packages/cli run test, yarn --cwd packages/cli build).
🟡 5 hunk-survived restates an inert file-level revert test-efficacy.ts: when any file-level revert probe came back inert, the hunk-survived message now appends "A file-level revert probe also came back inert, so this restates that gap at hunk granularity — read the two as one finding, not two." (the "note the redundancy in the finding text" option).
🟡 6 A red baseline is reported as a budget skip test-efficacy.ts: hunks now have their own skippedForBaseline counter, report field, and summary line ("N hunk probe(s) skipped: no probe file was green in the unmutated baseline"), mirroring mutants, instead of reusing skippedForBudget's "the mutants used the window".
🟡 7 splitDiffIntoHunks gives every hunk the first file's header test-efficacy.ts: fileHeader is re-captured at every diff --git boundary, so each hunk's patch carries its own file's header. Regression test: test-efficacy.test.ts "gives each hunk ITS OWN file header on a multi-file diff" (second patch names two.ts, not one.ts).
🟡 8 base-tree costs a second npm ci, not "one extra build" base-tree.ts header, lib/agent-briefs.ts, DESIGN.md, and SKILL.md now describe the cost as "an install and a build" (reused across the review once built) everywhere they previously said "one extra build" / "a second build" (the "docs should say 'an install and a build'" option). A grep for the old wording across packages/ returns nothing.

Findings 1–4 (the ones that produce wrong or missing output on ordinary inputs)
and finding 7 each carry a dedicated regression test asserting the corrected
behaviour. Findings 5 and 6 are reporting changes carried through the report
structure (verified by typecheck), and finding 8 is documentation wording.

Verification

Re-run against the current HEAD (67ac8aa2d, which contains 5054121b0):

  • npm run typecheck — passed
  • npx vitest run src/commands/review src/commands/review.test.ts (from packages/cli) — 42 files, 1164 passed | 3 skipped (the suite the review ran, plus the regression tests added by 5054121b0)
  • npm run build — passed
  • npx eslint on the eight files touched by 5054121b0 — clean
中文说明

无需新改动 —— 八条发现已在本分支上全部解决

感谢这份细致而有分寸的评审。其中的每一条发现都已由提交 5054121b0("fix(review): address the eight findings from live review of this branch")解决,该提交晚于被审的 head c0481dd,并且已经包含在本 PR 的分支中。本轮无需再做代码改动;下表中的每一项都是针对当前 HEAD 重新核验过的,而非仅凭提交说明轻信。

# 发现 本分支上的解决方式
🔴 1 BOLD_LINE_RE 灾难性回溯 test-plan.ts:正则现为 /^\*\*([^*\n]+)\*\*:?\s*$/ —— 不再有歧义的 \s*,也不再有惰性量词,因此扫描是线性时间;捕获组在使用点 .trim()bold?.[1]?.trim())。回归测试:test-plan.test.ts"scans a hostile unclosed-bold line in linear time"** + 50,000 个空格 → null;一旦回归,会挂到超时,而不是断言失败)。
🟠 2 当 hunk 在新建目录中新增文件时探针抛 ENOENT test-efficacy.tsfinally 中的恢复现在会先执行 mkdirSync(dirname(abs), { recursive: true })writeFileSync,因此裁定不再丢失,后续 hunk 也不再被标记为 inconclusive。回归测试:test-efficacy.integration.test.ts"restores a hunk-ADDED file whose parent directory the reverse apply removed"(针对真实 git,src/newdir/ 下的文件被完整恢复)。
🟠 3 base-tree 没有租约,而 Step 4 分片并行运行 base-tree.ts:增加了一条幂等快路径,以 .qwen-review-base-ok 标记文件(仅在构建成功后写入)加 HEAD == baseSha 校验为键,将已构建好的树原样返回,不做清除。并发分片复用同一棵已构建的树,于是最坏情况 —— 分片 B 清掉分片 A 正在做 A/B 的树、从而在 BASE 侧伪造出一个带确定性来源标签的差异 —— 不再发生。不同 SHA 的标记(两次运行之间发生了 rebase)会落回重新构建。回归测试:base-tree.test.ts"REUSES an already-built base tree instead of sweeping it"(两次调用 → 一次构建;错误 SHA 的标记不走快路径)。
🟠 4 npmScriptOf 把前置的 workspace flag 当成脚本名 test-plan.tsif (name.startsWith('-')) return null; —— 当 flag 位于脚本名之前时返回 unchecked("偏向沉默"),绝不会在正确的 Test Plan 上误报 no package defines this script。回归测试:test-plan.test.ts"is null when a FLAG precedes the script"npm --workspace=packages/cli run buildnpm -w packages/cli run testyarn --cwd packages/cli build)。
🟡 5 hunk-survived 重复了 inert 的整文件 revert test-efficacy.ts:当任一整文件 revert 探针返回 inert 时,hunk-survived 的文案现在会追加 "A file-level revert probe also came back inert, so this restates that gap at hunk granularity — read the two as one finding, not two."(即"在 finding 文案里点明冗余"这一选项)。
🟡 6 基线为红时被报告成预算跳过 test-efficacy.ts:hunk 现在有自己的 skippedForBaseline 计数器、报告字段和汇总行("N hunk probe(s) skipped: no probe file was green in the unmutated baseline"),与 mutant 对齐,而不再复用 skippedForBudget"the mutants used the window"
🟡 7 splitDiffIntoHunks 给每个 hunk 都套上第一个文件的头 test-efficacy.tsfileHeader 在每个 diff --git 边界处重新捕获,因此每个 hunk 的 patch 都带上自己文件的头。回归测试:test-efficacy.test.ts"gives each hunk ITS OWN file header on a multi-file diff"(第二个 patch 指向 two.ts,而非 one.ts)。
🟡 8 base-tree 的代价是第二次 npm ci,而非"一次额外构建" base-tree.ts 头部注释、lib/agent-briefs.tsDESIGN.mdSKILL.md 现在都把代价描述为 "an install and a build"(构建一次后在整个评审中复用),凡是此前写作"one extra build"/"a second build"之处均已更新(即"文档应改成 'an install and a build'"这一选项)。在 packages/ 全量 grep 旧措辞已无任何匹配。

第 1–4 条(在普通输入上就会产出错误或缺失输出的那几条)以及第 7 条,各自带有一条断言修正后行为的专门回归测试。第 5、6 条是贯穿报告结构的文案/字段改动(由 typecheck 验证),第 8 条是文档措辞。

验证

针对当前 HEAD(67ac8aa2d,包含 5054121b0)重新运行:

  • npm run typecheck —— 通过
  • npx vitest run src/commands/review src/commands/review.test.ts(在 packages/cli 下)—— 42 个文件、1164 项通过 | 3 项跳过(即评审所跑的套件,外加 5054121b0 新增的回归测试)
  • npm run build —— 通过
  • 5054121b0 触及的八个文件运行 npx eslint —— 无告警

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


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

@wenshao

wenshao commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Maintainer verification — built and run locally

Verified on a fresh worktree of 67ac8aa (npm ci + npm run build, Linux, Node v22.22.2), driving the built CLI at packages/cli/dist/index.js. Every item in the Reviewer Test Plan reproduces. Four findings came out of running the new capabilities against real PRs rather than reading them — one of which I think should be fixed before merge.


1. Reviewer Test Plan — 5/5 reproduce

Claim Result
npx vitest run src/commands/review src/commands/review.test.ts → 42 files, 1161 tests 42 files ✅ / 1167 tests (see Finding D)
DESIGN.md + SKILL.md gain the three sections ✅ present and accurate
test-plan on #8176 → finds ## Reviewer Test Plan, paths + npm commands reproduces, count unchecked ✅ exactly — 9 claims: 8 reproduces, 1 unchecked
base-tree on #8176available: true, built in …-base; cleanup reports Removed base worktree ✅ both
Non-blocking contract: Test Plan (not a blocker): …, never changes the event ✅ proven three ways

vitest

test-plan

base-tree built the merge base c3620bc7a in 2m57s, and --build-only did what it says — test: [], note Tests were not run (build-only). The idempotent fast path is real: a second invocation returned the same report in 0.56s instead of rebuilding.

cleanup

The non-blocking contract holds under all three states. Same state.json, same comments, only the test-plan report varies:

contract

event and cappedBy are identical whether the report is present, absent, or stale; a stale report produces a body byte-for-byte identical to no report at all. The clause only ever adds text.

2. The A/B harness reproduces #8176's manual table byte-for-byte

Both trees built by the new subcommands — merge base via qwen review base-tree, head via qwen review build-test --build-only — then the same script run against each packages/core/dist:

A/B

This matches the table in the PR description exactly. The capability does what it claims, and it is the first thing in this pipeline that produces a measurement rather than a reading.

3. Per-hunk probes run, and are non-vacuous

Run live against this PR's own diff (packages/cli, 41 hunks):

hunks

killed=2 survived=3 inconclusive=1 skippedForCap=35 — the killed/survived split is the control that shows the probe distinguishes gated changes from ungated ones, and the 35 capped hunks are reported rather than dropped. This is a real capability working outside its own test harness.

But two of the three survivors are false. That is finding A.


Finding A (please fix before merge) — hunk-survived can be emitted for a change a test in the diff does cover

The probe worktree resolves node_modules by walking up to the repo root (test-efficacy.ts:1361). That works for hoisted packages but not for workspace-nested ones, and this repo's committed package-lock.json nests several:

node_modules/ajv                 6.12.6      <- hoisted (eslint's)
packages/core/node_modules/ajv   8.20.0      <- nested, what core imports
packages/core/node_modules/diff  7.0.0       <- nested, no root copy at all

Any probe file that transitively imports packages/core/src/utils/schemaValidator.ts therefore collects zero tests in the probe tree. It is classified inconclusive, dropped from greenProbes, and the remaining files' green run becomes the hunk's verdict — survived.

packages/cli/src/commands/review.test.ts is exactly such a file, and it is the file that asserts the registration list the two flagged review.ts hunks create:

false survivor

1) probe worktree exactly as test-efficacy creates it   -> total=0   (Failed to resolve import "ajv/dist/2020.js")
2) same tree + the nested workspace deps                -> 5 passed
3) same tree + deps + the SAME hunk reverse-applied     -> 4 passed, 1 FAILED
                                                            "registers exactly the expected internal helper subcommands"

So the finding the run emitted —

packages/cli/src/commands/review.ts:46: reverting this hunk on its own leaves every affected test green … so it ships unprotected

— is false. The change is gated, by a test this diff itself edits (review.test.ts, +2 lines adding 'base-tree' and 'test-plan' to the expected list).

Why I think this blocks rather than waits. DESIGN.md states the asymmetry precisely: "a tree that will not compile without the hunk is inconclusive and never killed … scoring it as 'a test caught it' is the precise false assurance this whole command exists to remove." That is implemented for the mutated run. The baseline exclusion path has the opposite polarity — a probe file that cannot compile is silently dropped, and its absence is then converted into a positive claim that no test covers the change. Same false-assurance shape, pointed the other way.

Mutants have the same latent gap, but the safety-verb filter is narrow enough that it rarely fires (1 mutant on this diff). Per-hunk probes fire on every hunk (6 probes → 3 survivors, 2 provably false here), so this PR turns a latent gap into a routine generator of false Suggestion-severity review comments. And these are findings, not disclosures — they reach the PR author.

Cheapest fix that preserves the design: when a hunk's own collocated test file is not in greenProbes, the hunk is inconclusive, not survived. That keeps every honest survivor and drops exactly the class above. Making the probe tree resolve nested deps (an install, or linking packages/*/node_modules) is the more complete fix but costs real time per review.

Finding B (non-blocking, but worth a follow-up) — contradicted path rulings are noisy

I ran test-plan over the 14 most recent merged PRs plus this one. Every contradicted ruling I observed — 8 of 8, across 4 PRs — was a false positive, from six distinct causes:

PR Claim ruled contradicted What it actually is
#8215 QwenLM/qwen-code the owner/repo argument to fetch-pr
#8215 .qwen/tmp/review-pr-8176, …-base paths the Test Plan's own steps create
#8215 packages/cli/dist/index.js a build artifact (gitignored, exists only after npm run build)
#8199 release/v0.21.2 a git branch name in prose backticks
#8205 src/commands/review/lib/path-rules.test.ts working dir stated as prose ("From packages/cli:"), so the cd rule never fires
#8205 identity/d a fragment of sed -i '/never agree on identity/d' …
#8223 sdk-typescript/sdk-mcp-server.test.ts --root ./integration-tests sets the base dir

The blast radius is bounded exactly as designed — never blocks, never caps — but these render in the body on every verdict including Approve. This PR's own Approve body would carry four false no such file or directory notes (visible in the contract screenshot above, len=1637 vs 1015).

Given the design philosophy this file states ("a wrong comment costs more than a missing one"), the extraction looks a notch too eager. Suggestions, roughly in order of value: require a file-extension-bearing final segment before ruling a path contradicted; skip tokens that are .gitignored; skip the token immediately following fetch-pr/--repo; and treat any command carrying --root/-C/a prose-stated cwd as unresolvable (unchecked).

Finding C (nit) — the PR description's own evidence mislabels a field

The description reports "all 6 skippedForBudget when the probe baseline was red". In that branch the code sets hunksSkippedForBaseline, and my live #8176 run confirms it: skippedForBaseline: 6, skippedForBudget: 0. The accounting is honest — only the write-up names the wrong field.

Finding D (nit) — the Test Plan's count claim is stale

1161 claimed, 1167 observed (the two merges from main brought six more). Worth noting that this PR's own new rule handles it exactly right — differs, not contradicted, with both numbers side by side. Nice dogfood.


Verdict

The design is sound and the three capabilities all work, verified by running them rather than reading them: the A/B harness reproduces its claimed table byte-for-byte, test-plan rules #8176 exactly as the description says, and per-hunk probing produces a real killed/survived split on a live diff. The non-blocking contract is airtight.

Recommend merging once Finding A is addressed. It is a small, well-scoped change (survivedinconclusive when the hunk's collocated test was excluded from the baseline), and without it the headline capability emits false findings to PR authors on this repo deterministically — the one outcome the skill's design philosophy weighs heaviest. Findings B–D are fine as follow-ups.

中文版

Maintainer 本地构建验证

67ac8aa 的全新 worktree 上验证(npm ci + npm run build,Linux,Node v22.22.2),全部通过构建产物 packages/cli/dist/index.js 驱动。Reviewer Test Plan 的每一项都能复现。另有两个发现来自"把新能力真正跑在真实 PR 上"而不是读代码,其中一个我认为应在合并前修复。

1. Reviewer Test Plan —— 5/5 复现

断言 结果
npx vitest run src/commands/review src/commands/review.test.ts → 42 文件、1161 测试 42 文件 ✅ / 1167 测试(见发现 D)
DESIGN.md + SKILL.md 新增三节 ✅ 存在且准确
test-plan#8176 → 找到 ## Reviewer Test Plan,路径与 npm 命令判 reproduces,计数判 unchecked ✅ 完全一致 —— 9 条断言:8 reproduces、1 unchecked
base-tree#8176available: true,构建于 …-basecleanup 输出 Removed base worktree ✅ 两者均满足
非阻断契约:Test Plan (not a blocker): …,绝不改变 event ✅ 三种状态下均已证明

base-tree 用 2m57s 构建了 merge base c3620bc7a--build-only 行为正确 —— test: [],note 为 Tests were not run (build-only).。幂等快路径确实生效:第二次调用 0.56s 返回同一份报告,未重建。

非阻断契约在三种状态下都成立。相同 state.json、相同 comments,只改变 test-plan 报告:报告存在/缺失/过期时 eventcappedBy 完全一致;过期报告产生的正文与"没有报告"逐字节相同。该子句只会增加文本。

2. A/B harness 逐字节复现了 #8176 的人工对照表

两棵树均由新子命令构建 —— merge base 用 qwen review base-tree,head 用 qwen review build-test --build-only —— 然后用同一个脚本分别跑各自的 packages/core/dist。结果与 PR 描述中的表格完全一致。该能力名副其实,也是本流水线里第一个产出"测量"而非"读法"的东西。

3. Per-hunk 探针确实运行,且非空洞

在本 PR 自己的 diff(packages/cli,41 个 hunk)上实跑:killed=2 survived=3 inconclusive=1 skippedForCap=35。killed/survived 的分野就是控制组,说明探针能区分"被测试守护"与"未被守护"的改动,35 个超 cap 的 hunk 也如实上报而非静默丢弃。这是该能力在自身测试套件之外真正工作的证据。

但三个 survivor 中有两个是假的 —— 即发现 A。

发现 A(建议合并前修复)—— hunk-survived 可能对"diff 中确实有测试覆盖"的改动误报

探针 worktree 依靠向上查找解析 node_modulestest-efficacy.ts:1361)。这对被提升(hoisted)的包成立,对嵌套在 workspace 下的包不成立,而本仓库提交的 package-lock.json 里就有若干嵌套:

node_modules/ajv                 6.12.6      <- 提升的(eslint 的)
packages/core/node_modules/ajv   8.20.0      <- 嵌套,core 实际 import 的
packages/core/node_modules/diff  7.0.0       <- 嵌套,根本没有根级副本

因此任何传递性 import 了 packages/core/src/utils/schemaValidator.ts 的探针文件,在探针树中收集到 0 条测试,被判 inconclusive、从 greenProbes 中剔除,于是剩下那些文件的绿色结果就成了该 hunk 的裁定 —— survived

packages/cli/src/commands/review.test.ts 正是这样一个文件,而它恰恰就是断言那两个被标记的 review.ts hunk 所建立的注册列表的文件:

1) 完全按 test-efficacy 的方式创建的探针树   -> total=0   (Failed to resolve import "ajv/dist/2020.js")
2) 同一棵树 + 嵌套 workspace 依赖            -> 5 passed
3) 同一棵树 + 依赖 + 反向应用同一个 hunk      -> 4 passed,1 FAILED
                                                "registers exactly the expected internal helper subcommands"

所以这次运行产出的 finding —— "反向应用该 hunk 后所有受影响测试仍为绿,故它未受保护地上线" —— 是假的。该改动确实被守护,守护它的正是本 diff 自己修改的测试(review.test.ts,+2 行,把 'base-tree''test-plan' 加入期望列表)。

为什么我认为这该阻断而非延后。 DESIGN.md 把这条不对称写得很清楚:"去掉该 hunk 后无法编译的树判 inconclusive绝不killed …… 把它记为'有测试抓住了'正是本命令存在的意义所要消除的那种虚假保证。"这一条在变异运行路径上实现了;但基线剔除路径的极性恰好相反 —— 无法编译的探针文件被静默剔除,而它的缺席随后被转换成"没有测试覆盖此改动"的正面论断。同样的虚假保证形状,方向相反。

Mutant 存在同样的潜在缺口,但 safety-verb 过滤足够窄,很少触发(本 diff 只有 1 个 mutant)。Per-hunk 探针则对每个 hunk 都触发(6 次探测 → 3 个 survivor,其中 2 个可证明为假),因此本 PR 把一个潜在缺口变成了常态化的假 Suggestion 级评审意见生成器。而且这些是 finding、不是 disclosure —— 它们会送达 PR 作者。

保持设计前提下最省的修法:当某 hunk 自身的同位测试文件不在 greenProbes 中时,该 hunk 判 inconclusive 而非 survived 这保留了所有诚实的 survivor,只丢弃上述这一类。让探针树能解析嵌套依赖(安装,或链接 packages/*/node_modules)是更彻底的修法,但每次评审都要付出实打实的时间。

发现 B(非阻断,但值得跟进)—— contradicted 路径裁定噪声偏大

我把 test-plan 跑在最近 14 个已合并 PR 加本 PR 上。我观察到的每一条 contradicted 裁定 —— 4 个 PR 上共 8 条 —— 全部是误报,来自六种不同成因:

PR 被判 contradicted 的断言 实际是什么
#8215 QwenLM/qwen-code fetch-prowner/repo 参数
#8215 .qwen/tmp/review-pr-8176…-base Test Plan 自己的步骤要创建的路径
#8215 packages/cli/dist/index.js 构建产物(被 gitignore,npm run build 后才存在)
#8199 release/v0.21.2 散文反引号中的 git 分支名
#8205 src/commands/review/lib/path-rules.test.ts 工作目录写在散文里("From packages/cli:"),cd 规则不触发
#8205 identity/d sed -i '/never agree on identity/d' … 的片段
#8223 sdk-typescript/sdk-mcp-server.test.ts --root ./integration-tests 设定了基准目录

影响面确实如设计所限 —— 不阻断、不封顶 —— 但它们会在包括 Approve 在内的所有 verdict 的正文中渲染。本 PR 自己的 Approve 正文就会带上四条假的 no such file or directory(见上文 contract 截图,len=1637 对比 1015)。

考虑到该文件自己声明的设计哲学("一条错误的评论比一条缺失的评论代价更大"),当前抽取略显激进。建议(按价值排序):仅当路径末段带文件扩展名时才允许判 contradicted;跳过被 .gitignore 的 token;跳过紧跟 fetch-pr--repo 之后的 token;对携带 --root-C/散文式 cwd 的命令一律视为不可解析(unchecked)。

发现 C(小问题)—— PR 描述自身的证据写错了字段名

描述写的是"探针基线为红时 6 个全部记 skippedForBudget"。该分支里代码设置的是 hunksSkippedForBaseline,我实跑 #8176 的结果也印证了:skippedForBaseline: 6, skippedForBudget: 0。账目本身是诚实的 —— 只是行文写错了字段。

发现 D(小问题)—— Test Plan 的计数已过期

声称 1161,实测 1167(两次从 main 的合并带来了 6 条)。值得一提的是本 PR 自己的新规则对此处理得完全正确 —— 判 differs 而非 contradicted,并把两个数字并列呈现。很好的 dogfood。

结论

设计是站得住的,三个能力也都工作,并且是"跑出来"而非"读出来"的:A/B harness 逐字节复现了其声称的对照表,test-plan#8176 的裁定与描述完全一致,per-hunk 探测在真实 diff 上产出了真实的 killed/survived 分野。非阻断契约无懈可击。

建议在处理完发现 A 后合并。 那是一处小而边界清晰的改动(当 hunk 的同位测试被排除在基线之外时,survivedinconclusive);不处理的话,在本仓库上这个头牌能力会确定性地向 PR 作者发出假 finding —— 而这正是该 skill 设计哲学中权重最高的那一种后果。发现 B–D 作为后续跟进即可。

@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. 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. Not reviewed: You are review agent reverse-audit — Reverse audit agen... — the agent made no tool call: it read nothing. Not reviewed: This PR adds three verification capabilities to /review: ... — pointed at diff lines it never opened: it made tool calls, but none of them read the diff. Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies. Not reviewed: verification — its prompt was built, but no agent was launched with it, so the posted findings cannot be counted as verified.

中文说明

已审查。 建议见行内评论。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。 未审查:You are review agent reverse-audit — Reverse audit agen...——该 agent 未发起任何工具调用:它什么都没读。 未审查:This PR adds three verification capabilities to /review: ...——它被指向 diff 的行却从未打开:有工具调用,但没有一次读取 diff。 未审查:反向审计——有审计 agent 运行并打开了自己的 brief,但没有 agent 是用 CLI 构建的 prompt 启动的——启动 prompt 是手写的,agent 实际被要求做的并不是本 skill 所认证的内容。 未审查:验证——它的 prompt 已构建,但没有 agent 用它启动,发布的发现不能算作已验证。

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/commands/review/test-plan.ts Outdated
Comment thread packages/cli/src/commands/review/test-efficacy.ts Outdated
Comment thread packages/cli/src/commands/review/test-plan.test.ts
@wenshao

wenshao commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review: feat(review): Test Plan claim check, base-tree A/B harness, per-hunk probes

Overview

Three independent capabilities plus two supporting fixes:

Change Shape
qwen review test-plan Extracts path / npm-script / test-count claims from the PR body's Test Plan and rules each against the reviewed tree. Wired into compose-review via testPlanGate — disclosed on every verdict, never blocks, never caps.
qwen review base-tree Builds the merge base in a sibling worktree (<wt>-base), reusing build-test's build set and widening loop behind a new --build-only flag. Verifier-on-demand, per finding.
per-hunk probes Third probe kind in test-efficacy: reverse-apply one hunk, re-run affected tests, attribute a still-green suite to that specific change.
trimOutput Rescues runner summary lines from the omitted middle.
observedTestCounts Strips ANSI SGR before parsing counts.

The design discipline is consistent with the rest of the pipeline and is the strongest thing about the PR: contradicted only where a ruling is sound, differs (never contradicted) for counts, inconclusive (never killed) on a compile failure, and every capped / budget-skipped / baseline-skipped count surfaced rather than silently zeroed. discardWorktree + worktreeCreateFailureDetail extracted to lib/worktree.ts for the second consumer rather than copied, and baseWorktreePath living beside probeWorktreePath so creator and sweeper cannot drift, are the right calls.

Test coverage is unusually good: ~120 new cases including real-git integration tests for hunk apply/restore (parent-directory recreation, patch-will-not-apply, file-absent), the base-tree concurrency fast path and lock, and the testPlanGate never-caps contract from six angles.

Note on scope: I reviewed the diff by reading; I did not execute the suite (the PR branch is not checked out here). The findings below were reproduced against extracted copies of the functions.


Findings

1. trimOutput no longer bounds its output — Suggestion, Source: [review]

packages/cli/src/commands/review/build-test.ts:813

RUNNER_SUMMARY_RE = /^\s*(?:Tests?|Test Files):?\s+\d/ matches far more than a runner summary — Test 42: renders the thing and Test 1 of 200 both pass it — and rescued is joined into the marker with no cap. trimOutput's entire contract is that the report handed to Agent 7 (and stored in the JSON the orchestrator reads) stays bounded at KEEP_HEAD + KEEP_TAIL.

Reproduced against a copy of the function, 40 000 lines of Test <n>: renders the widget correctly:

raw length      : 1628893
trimmed length  : 1628981

The trim is a no-op — slightly worse than one, because of the marker. The pre-existing MODULE_ERROR_RE rescue had the same unbounded shape, but Cannot find module '…' is a narrow enough pattern that it never mattered; this one is not.

Two independent fixes, and I'd take both:

  • Require a verdict keyword, which every real summary carries:
    const RUNNER_SUMMARY_RE =
      /^\s*(?:Tests?|Test Files):?\s+\d[^\n]*\b(?:passed|failed|skipped|todo|total|pending)\b/;
    Checked against every case in the new test plus the ANSI-stripped one — all four real summaries keep, both false positives drop.
  • Cap rescued (last ~20 lines) so neither pattern can defeat the bound in future.

2. A #-prefixed line with no space truncates the Test Plan section — Suggestion, Source: [review]

packages/cli/src/commands/review/test-plan.ts:3770 (HEADING_LINE_RE = /^(#{1,6})(.*)$/)

ATX headings require whitespace after the hashes; GitHub renders #8176 also covers this path. as plain text. This regex reads it as a level-1 heading, and the section scanner breaks on the first heading of the same-or-higher level — so an issue reference at column 0 inside a Test Plan silently ends the section there.

Reproduced against a copy of extractTestPlanSection:

input:
  ## Reviewer Test Plan
  - Run `npm run build` first.
  #8176 also covers this path.
  - Run `packages/cli/src/commands/review/test-plan.test.ts` and expect 12 tests to pass.

content: "- Run `npm run build` first."

Everything after the reference — one path claim and one count claim — is never extracted. The bias is toward silence, which matches the design philosophy, but it is silence with no disclosure: the report says checked 1 claim(s) and reads as a complete check.

/^(#{1,6})(\s.*|)$/ (or #{1,6}(?=\s|$)) fixes it and also removes the matching false-start risk on #Test Plan.

3. base-tree's build lock has no staleness handling — Suggestion, Source: [review]

packages/cli/src/commands/review/base-tree.ts:579-588

mkdirSync(lock) is the right atomic test-and-set, but two gaps:

  • Any mkdirSync failure is reported as another probe is building the base tree right now. EACCES, ENOSPC, ENOTDIR all get a message that says the opposite of what happened, and the note is rendered verbatim to the reviewer. Gate on err.code === 'EEXIST' and report the rest as what they are.

  • A builder killed by a signal never runs its finally, so the lock survives. cleanup.ts:427 sweeps it — but only at the end of a review that reaches cleanup. A review killed mid-build leaves <wt>-base.lock behind, and because fetch-pr recreates .qwen/tmp/review-pr-<n> without touching its siblings, every later review of the same PR finds the stale lock and reports "another probe is building" for its entire duration. Nothing ever clears it but a manual rm.

    A mtime check would close it: treat a lock older than, say, 2 × timeout as abandoned and take it over. The blast radius of a wrong takeover is a duplicated build, which is what the lock is saving in the first place — strictly better than a permanently dead capability.

The cleanup sweep and its test are good; the gap is the window before cleanup runs.

4. Hunk restore does not restore the file mode — Nice to have

packages/cli/src/commands/review/test-efficacy.ts (runOneHunkProbe's finally)

splitDiffIntoHunks carries the whole file header into each hunk patch, including old mode / new mode lines when the PR changes a file's mode. git apply --reverse flips the mode back; the restore writes content only, so the file keeps the reverted mode for every later probe in the same run. Only bites a mode-changing diff (rare, and mostly on shell scripts — which script-lint covers anyway), but a chmod from a statSync taken beside the readFileSync would close it for two lines.


Smaller notes

  • PLAN_NAME_RE includes a bare \btesting\b, so ## Testing philosophy or ## Testing notes is claimed as the Test Plan. Harmless given everything downstream is non-blocking, but it means a PR with both sections may check the wrong one — the scanner takes the first match.
  • ruleCommand returning contradicted for a command this review ran and that failed duplicates what Agent 7 already files as a [build] / [test] Critical, now restated as a "Test Plan (not a blocker)" note. Not wrong, just two voices on one fact.
  • base-tree's markers (.qwen-review-base-ok / .qwen-review-base-failed) are written inside the worktree. Fine as-is since the tree is disposable and never diffed — worth a one-line comment saying so, since the same trick in the review worktree would be a bug.
  • 3 903 added lines across three independent features in one PR. Each of the three lands cleanly and the shared refactor (lib/worktree.ts) genuinely spans two of them, so this is a process observation rather than a defect — but test-plan in particular is self-contained and would have been reviewable on its own.

Correctness spot-checks that came back clean

  • test-plan path handling against a hostile PR body: PATH_RE admits no leading /, normalize + startsWith('..') rejects traversal, C:/… does not match, and the only filesystem op is existsSync. No read of claimed content.
  • ReDoS: the rewritten BOLD_LINE_RE is linear; PLAN_NAME_RE and the bounded (?<!Files\s{1,20}) lookbehind are fine; codeSpans' lazy fence scan is bounded in practice by the 65 536-char body cap and the fact that an unclosed fence terminates the scan.
  • hunksSkippedForBudget = hunkCandidates.length - hunkResults.length is correct at the break — the per-hunk inconclusive pushes above it are resolved, not pending — and hunkCandidates.slice(hunkResults.length) in the catch is index-aligned by construction.
  • splitDiffIntoHunks' startLine offset walk excludes - and \ No newline exactly as parseAddedLines does; selectHunkProbes measures the mutant-overlap range from the header's new-side start rather than the anchored startLine, which is the right call and is pinned by a test.
  • Choosing hunk candidates outside the candidates.length > 0 branch is the load-bearing decision of the whole third probe kind, and the comment at that site says exactly why.
  • runBuildTest's ok: true is correctly not trusted on its own in base-tree — the toolchain !== 'npm' || build.length === 0 guard is what keeps an unbuilt base from being read as a behavioural difference. That is the sharpest bug this PR avoids.

Verdict

Well-built, well-argued, and unusually well-tested. Nothing here blocks the design. Finding 1 is the one I would fix before merge — it silently removes a bound the rest of the pipeline depends on. Findings 2 and 3 are cheap fixes with real failure modes behind them.

中文说明

概述

三个独立能力加两个配套修复:test-plan(抽取 PR 描述里的路径 / npm script / 测试数断言并逐条裁定,经 testPlanGate 接入 compose-review,披露但绝不阻断、绝不封顶)、base-tree(在 sibling worktree 构建 merge base,新增 --build-only,按 finding 触发)、test-efficacy 的 per-hunk 反向应用探针,以及 trimOutput 抢救 runner 汇总行 / observedTestCounts 剥离 ANSI。

纪律与流水线其余部分一致,也是本 PR 最强的地方:只在裁定可靠处判 contradicted,计数只判 differs,编译失败只判 inconclusive,所有 cap / budget / baseline 跳过数一律上报而非静默归零。discardWorktree 抽到 lib/worktree.tsbaseWorktreePathprobeWorktreePath 并置,都是正确取舍。测试覆盖罕见地扎实(约 120 个新用例,含真实 git 的 hunk 应用/恢复集成测试与 base-tree 并发快路径/锁)。

说明:我以阅读方式审查 diff,未执行套件(本地未检出该分支);下面两条已用函数副本复现。

发现

1. trimOutput 不再有上界 —— Suggestionbuild-test.ts:813

RUNNER_SUMMARY_RE 过宽(Test 42: renders the thingTest 1 of 200 均命中),且 rescued 无数量上限。用函数副本复现:4 万行 Test <n>: … 输入 1 628 893 字符,输出 1 628 981 字符——截断完全失效。建议同时做两件事:正则加一个结果词(\b(?:passed|failed|skipped|todo|total|pending)\b,已验证 PR 自带测试的四种真实汇总全部保留、两个误报全部剔除),并给 rescued 加条数上限。

2. 无空格的 # 行会截断 Test Plan 段 —— Suggestiontest-plan.ts:3770

ATX 标题要求 # 后有空白,GitHub 把 #8176 also covers this path. 渲染为纯文本,但 HEADING_LINE_RE 把它读成 h1 并终止段落。复现:Test Plan 中该行之后的一个路径断言与一个计数断言全部未被抽取,而报告写 checked 1 claim(s),读起来像完整检查。建议改为 /^(#{1,6})(\s.*|)$/

3. base-tree 的构建锁没有过期处理 —— Suggestionbase-tree.ts:579-588

其一,mkdirSync 的任何失败(EACCES/ENOSPC/ENOTDIR)都被报成"另一个 probe 正在构建",与事实相反且原文透传给 reviewer——建议只在 err.code === 'EEXIST' 时这样说。其二,被信号杀死的构建者不会执行 finally,锁残留;cleanup 会清扫,但只在能走到 cleanup 的 review 末尾。由于 fetch-pr 重建 .qwen/tmp/review-pr-<n> 时不动其 sibling,同一 PR 的后续每次 review 都会撞上残留锁并在整个 review 期间报"另一个 probe 正在构建",除手工 rm 外无人清理。建议用 mtime 判定(超过 2 × timeout 视为废弃并接管)——误接管的代价只是重复一次构建,严格优于能力永久失效。

4. hunk 恢复不还原文件模式 —— Nice to have

splitDiffIntoHunks 会把完整 file header(含 old mode/new mode)带进每个 hunk patch,反向应用会翻回旧模式,而 finally 只写回内容。仅影响改动了模式的 diff;在 readFileSync 旁取一次 statSyncchmod 即可。

其他

PLAN_NAME_RE 里裸的 \btesting\b 会把 ## Testing philosophy 当成 Test Plan(取首个匹配);ruleCommand 对"本次 review 跑过且失败"的命令判 contradicted,与 Agent 7 已报的 [build]/[test] Critical 重复叙述同一事实;base-tree 的 marker 写在 worktree 内部(这里没问题,但值得一行注释说明为何在 review worktree 里同样做法就是 bug);3 903 行、三个独立能力放在一个 PR——这是流程观察而非缺陷,但 test-plan 完全自洽,本可单独评审。

复核通过的点

对抗性 PR body 下的路径处理(PATH_RE 不收前导 /normalize + startsWith('..') 挡住穿越,唯一文件系统操作是 existsSync);ReDoS(重写后的 BOLD_LINE_RE 线性,有界 lookbehind,codeSpans 的惰性 fence 扫描在 65 536 字符上限下可接受);hunksSkippedForBudget 的差值与 catch 里的 slice 索引对齐;splitDiffIntoHunksstartLine 偏移与 parseAddedLines 一致,selectHunkProbes 用 header 起点而非 startLine 量算重叠范围;hunk 候选在 candidates.length > 0 分支之外选取——这是第三种探针成立的关键决策,注释也写清了原因;以及 base-tree 不单信 runBuildTestok: true,用 toolchain !== 'npm' || build.length === 0 挡住"没构建的树被读成行为差异"——这是本 PR 避开的最锋利的一个 bug。

结论

设计扎实、论证充分、测试少见地到位,没有阻断性问题。合并前建议先修 1(它悄悄取消了流水线依赖的一个上界);2 和 3 修复成本低但背后的失败模式真实。

wenshao added a commit that referenced this pull request Aug 1, 2026
From the #8215 review: trimOutput's summary rescue is capped at 40 lines
(uncapped, 40k lines of 'Test <n>: …' prose made the whole trim a no-op —
1.6MB in, 1.6MB out); a spaceless # line is prose per the ATX rule GitHub
applies (a pasted #!/bin/bash outside a fence once ended the Test Plan
mid-body), with the bare-#-run crash on the closing scan guarded; a
base-tree build lock older than 30 minutes is a corpse and is swept
rather than reporting busy for the rest of the review.

From the #8261 review: a pasted diff's BODY lines shed no path claims
(-packages/old/gone.ts matched PATH_RE and ruled a false contradicted —
the syntax-line filter alone was vacuous against the real failure mode);
a gitignored file that nonetheless EXISTS rules reproduces (the ignore
guard only ever downgrades a would-be contradiction); test-delta's note
now explains each unattributed command (timeout vs toolchain failure)
instead of leaving the reader to infer why it is in neither list.
…eep stale build locks

The three 8215-layer findings from the latest review, fixed at this
layer (they were first patched further up the stack, where the reviewer
of THIS PR cannot see them):

- trimOutput's summary rescue is capped at 40 lines — uncapped, 40k
  lines of 'Test <n>: …' prose voided the trim entirely (measured 1.6MB
  in, 1.6MB out) and the bounded-output contract is the whole point.
- A '#' with no following whitespace is prose, not a heading (the ATX
  rule GitHub applies): '#8176', '#tag', an unfenced '#!/bin/bash' no
  longer end the Test Plan section mid-body; the bare-#-run crash on
  the closing scan is guarded.
- A base-tree build lock older than 30 minutes is a corpse left by a
  killed builder — swept and rebuilt instead of reporting busy for the
  rest of the review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.

Suppressed comments (3)

packages/cli/src/commands/review/test-efficacy.ts:1376

  • The restore path writes the saved content back as a UTF-8 string, which can change bytes and will not restore non-UTF-8 content losslessly. If original is stored as a Buffer, write it back as bytes to guarantee exact restoration.
    mkdirSync(dirname(abs), { recursive: true });
    writeFileSync(abs, original, 'utf8');
  }

packages/cli/src/commands/review/base-tree.ts:200

  • The failed-build reuse fast path only checks the .qwen-review-base-failed marker contents, not that the worktree is still actually on baseSha. If the base tree was modified (e.g., someone checked out a different commit while investigating the failure), later probes will incorrectly treat the failure as a settled answer and skip rebuilding.
  try {
    if (
      existsSync(tree) &&
      readFileSync(failedMarker(), 'utf8').trim() === baseSha
    ) {
      return {
        available: false,
        path: tree,
        baseSha,
        build: null,
        note:
          `the base tree at ${baseSha.slice(0, 9)} already failed to build (an earlier probe measured it); ` +
          'an A/B is not available for this review (infrastructure, never a finding against the PR)',
      };
    }

packages/cli/src/commands/review/test-efficacy.ts:1335

  • runOneHunkProbe reads the original file as UTF-8 text. If the probed file is not valid UTF-8 (or is a text file where exact bytes matter), the read/restore can corrupt the file and contaminate later probes. Since this function’s contract is to restore the tree exactly, it should preserve raw bytes.

This issue also appears on line 1374 of the same file.

  let original: string;
  try {
    original = readFileSync(abs, 'utf8');
  } catch (e) {

@wenshao

wenshao commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review — feat(review): Test Plan claim check, base-tree A/B harness, per-hunk probes

Reviewed at cdff5693b (21 files, +3962/−76). Verdict: approve with one substantive suggestion. The three capabilities are well-shaped, the non-blocking contracts are enforced in code rather than only in prose, and the test suites are unusually thorough for new surface area. One real gap, plus a few nits.

What I verified (not just read)

  • The Test Plan's own command. cd packages/cli && npx vitest run src/commands/review src/commands/review.test.ts in a fresh worktree of the PR head: 42 files pass, 1213 tests pass. The Test Plan claims 1161 — the two follow-up commits on this branch added tests after the body was written. Under this PR's own rules that is differs, never contradicted, which is exactly the case the design argues for. (node_modules was symlinked from an existing checkout rather than npm ci'd; the PR touches neither package.json nor the lockfile.)
  • extractTestPlanSection / extractClaims against the live bodies of feat(review): Test Plan claim check, base-tree A/B harness, per-hunk probes #8215 and feat(core): Normalize tool-call terminal telemetry #8176. Both find ## Reviewer Test Plan. feat(core): Normalize tool-call terminal telemetry #8176 yields exactly the four vitest paths, the packages/core cd base, three npm commands and the 471 tests count the PR body predicts. No false path claims: --repo QwenLM/qwen-code, origin, and packages/cli/dist/index.js are all correctly excluded by the evidence bar / exclusion list. The heuristics are genuinely biased toward silence, as claimed.
  • splitDiffIntoHunks / selectHunkProbes edge cases. Multi-hunk startLine anchoring at the first added line (@@ -1,4 +1,5 @@ with two leading context lines → startLine: 3) is right; mode-change-only and binary entries return []; added/deleted files are skipped. The \n--- /dev/null / \n+++ /dev/null checks are column-0-anchored, so a source file whose added content contains diff text (+--- /dev/null) is correctly not mistaken for a file deletion — I tried to break that and could not.

Suggestion — per-hunk probes have no file-kind or content eligibility filter

hunkProbeInputs is fed revert in full, while mutant selection first narrows with MUTANT_SOURCE_RE (packages/cli/src/commands/review/test-efficacy.ts:1577):

const mutantFiles = revert.filter(
  (p) => MUTANT_SOURCE_RE.test(p) && !DECLARATION_FILE_RE.test(p),
);
// …
hunkProbeInputs(worktree, base, headSha, revert, probes, candidates)   // ← unfiltered

revert is kind === 'source', and classifyPath deliberately keeps src-tree markdown as source (the bundled skill prompts — planTestEfficacy's own comment says so). So a prose hunk in packages/core/src/skills/bundled/review/SKILL.md is an eligible hunk-probe candidate. Confirmed by running selectHunkProbes directly:

classifyPath('packages/core/src/skills/bundled/review/SKILL.md') = source
planTestEfficacy(...).revert = ["packages/core/src/skills/bundled/review/SKILL.md"]
selected = [{ file: "packages/core/src/skills/bundled/review/SKILL.md",
              index: 0, header: "@@ -10,3 +10,4 @@", startLine: 11, … }]

Nothing filters hunk content either, so a comment-only hunk in a .ts file is also a candidate:

comment-only selected = [{ file: "src/x.ts", header: "@@ -5,3 +5,5 @@", startLine: 6, … }]

Two costs when one of these is selected:

  1. A full vitest run is spent out of a cap of 6 that is already shared with the mutants' budget.
  2. The run comes back green (reverting prose cannot turn a suite red), so the verdict is survived and findings[] gains: "reverting this hunk on its own leaves every affected test green. No test in this diff fails when this particular change is undone — confirm an existing test covers it, or add one." — a Source: [test] Suggestion filed against a paragraph of documentation or a comment block.

That is the "a wrong comment costs more than a missing one" failure this skill's design explicitly optimises against, and the mutants already avoid it with the exact filter that is missing here. On this PR it happens not to fire — 21 files, and the hasNewTests-preferred pool overflows the cap of 6 before the two .md files are reached — but a PR that edits a bundled SKILL.md plus one .ts file is a routine shape in this repo, and there the .md hunks are all that is left to spend the cap on.

Suggested fix, both halves cheap:

  • Apply MUTANT_SOURCE_RE && !DECLARATION_FILE_RE to hunkProbeInputs' sources argument, same as the mutants (or filter inside selectHunkProbes, which keeps the exported function honest on its own).
  • Skip a hunk whose +/- lines are entirely blank or comment-only. selectMutants already has the machinery to recognise a non-statement line.

test-efficacy.test.ts's selectHunkProbes block has cases for deleted files, added files and mutant overlap — a .md-source case and a comment-only case would slot in beside them.


Nits

  • runOneHunkProbe does not take dependencyRoot (test-efficacy.ts:1766). runOneMutant gained the parameter and the call site passes worktree; the hunk probe calls runProbeSuite(probeTree, greenProbes, mutantDeadline, now) with the parameter defaulted, so findVitestBin(probeTree) runs instead of findVitestBin(worktree). It works today only because the baseline already farmed node_modules into the probe tree, and the createRequire up-walk would find the repo-root copy otherwise — but the hunk probe can end up resolving a different vitest than the baseline and mutants it is being compared against. One-line symmetry fix.
  • Stale comment at test-efficacy.ts:1564: "node_modules resolves without a per-tree install because the probe tree is nested under the repo, so Node walks up to the repo-root node_modules" — now contradicted three lines of setup later by exposeDependencies, which deliberately farms links into the probe tree. Worth updating, since the up-walk is now the fallback rather than the mechanism. (Related, and fine as-is: the farm exposes only root node_modules; non-hoisted packages/*/node_modules still fall through to the up-walk. No regression, but the report's "dependency farm" note reads as if the tree were self-sufficient.)
  • base-tree reports every mkdirSync(lock) failure as contention (base-tree.ts:226). EACCES, ENOSPC or a missing parent all produce "another probe is building the base tree right now — retry when its marker appears", and the marker never appears. Distinguishing EEXIST from the rest would keep the note honest — this command is careful everywhere else about not stating more than it observed.
  • The 30-minute lock-staleness sweep (base-tree.ts:219) removes only the lock; the next builder then discardWorktrees the tree a still-live builder is using. npm ci + full build on a cold CI runner is not comfortably under 30 minutes on every platform. The comment acknowledges the race for the first build; this widens it. A mtime touch from the builder, or simply raising the ceiling, would close most of it.
  • TestPlanClaim.text is documented as "the claim as the author wrote it, for quoting back", but the cd-base rewrite pushes ${base}/${t} (test-plan.ts:334), so a Test Plan that says src/commands/review.test.ts is quoted back to the author as packages/cli/src/commands/review.test.ts. More useful, probably — but the doc comment and compose-review's verbatim-quoting contract disagree, and it is the composed body the author reads.
  • HEADING_LINE_RE requires the # at column 0. CommonMark and GitHub allow up to three leading spaces. Safe direction (an indented Test Plan is silently not found rather than mis-ruled), so purely a note.

Elsewhere: good

  • The non-blocking contract for testPlanGate is enforced on all three composeReview return paths (REQUEST_CHANGES, the approve short-circuit, and the general clause list), not just the one that is easy to test — and the stale-diffHash drop plus the MAX_NOTES cap mean a stale or chatty report degrades to silence rather than noise.
  • base-tree's refusal to stamp available on ok: true with an empty build[] is the right call and easy to have missed: a merge base that maps to no package would otherwise let an A/B read "nothing was built" as a behavioural difference.
  • The hunk-survived message's restatesInert de-duplication against a file-level inert verdict is a nice touch — that pair would otherwise read as two independent findings about the same gap.
  • Security surface is clean: the PR body is untrusted input but only ever fed to regexes and existsSync; gh is invoked argv-style; claimed commands are never executed; base-tree builds merge-base (base-branch, trusted) code.
中文

审阅 cdff5693b(21 文件,+3962/−76)。结论:建议合并,一处实质性改进意见。三项能力设计扎实,非阻断契约由代码而非文档保证,新增面的测试覆盖相当充分。

实测(非仅阅读)

  • 跑了 Test Plan 自己的命令:42 文件、1213 项测试通过。Test Plan 声明 1161——分支上后两个提交又加了测试。按本 PR 自己的规则这应判 differs 而非 contradicted,正是设计所论证的情形。
  • feat(review): Test Plan claim check, base-tree A/B harness, per-hunk probes #8215feat(core): Normalize tool-call terminal telemetry #8176 的实时正文跑 extractTestPlanSection/extractClaims:两者都找到 ## Reviewer Test Planfeat(core): Normalize tool-call terminal telemetry #8176 恰好产出正文预言的四个 vitest 路径、packages/core 的 cd 基址、三条 npm 命令与 471 tests 计数;--repo QwenLM/qwen-codeorigindist/index.js 均被正确排除。抽取确实偏向沉默。
  • splitDiffIntoHunks/selectHunkProbes 边界:多 hunk 的 startLine 锚在首个新增行上正确;mode-only 与二进制条目返回 [];新增/删除文件被跳过;\n--- /dev/null 锚在列 0,所以「新增内容里含 diff 文本」的源文件不会被误判为删除——试图构造反例未果。

改进意见:per-hunk 探针缺少文件类型与内容的准入过滤

hunkProbeInputs 拿到的是完整的 revert,而 mutant 选择先用 MUTANT_SOURCE_RE 收窄(test-efficacy.ts:1577)。revertkind === 'source',而 classifyPath 有意把 src 树内的 markdown 留作 source(bundled skill prompt)。因此 packages/core/src/skills/bundled/review/SKILL.md 的一段散文 hunk 是合格候选——已直接调用 selectHunkProbes 证实。hunk 内容也没有过滤,.ts 中的纯注释 hunk 同样合格。

代价有二:一次完整 vitest 运行占掉与 mutant 共享的 6 个名额之一;且必然返回绿色 → 判 survived → 产出一条 Source: [test] 的 Suggestion,让作者「为这段散文/注释补一个测试」。这正是本 skill 一贯要规避的「错误评论比缺失评论代价更大」,而 mutant 侧已有的正是这里缺的那个过滤。本 PR 恰好不会触发(21 个文件,preferred 池在轮到两个 .md 之前就填满了 cap=6),但「改一份 bundled SKILL.md + 一个 .ts」在本仓库是常见 PR 形态,那时 .md 的 hunk 就是唯一能花掉 cap 的东西。

建议:把 MUTANT_SOURCE_RE && !DECLARATION_FILE_RE 也应用到 hunkProbeInputssources(或放进 selectHunkProbes 内部);并跳过 +/- 行全为空行或注释的 hunk。

小问题

  • runOneHunkProbe 未接收 dependencyRoottest-efficacy.ts:1766),与 runOneMutant 不对称;今天能工作只是因为 baseline 已把依赖农场铺进 probe 树。一行修复。
  • test-efficacy.ts:1564 的注释已过时——exposeDependencies 之后,up-walk 是兜底而非机制。
  • base-tree.ts:226mkdirSync(lock) 的所有失败都报成「另一个探针正在构建」;EACCES/ENOSPC 下 marker 永不出现。
  • base-tree.ts:219 的 30 分钟锁失效清扫只删锁,随后的 discardWorktree 会删掉仍在使用中的树;冷 CI 上 npm ci + 构建未必稳稳低于 30 分钟。
  • TestPlanClaim.text 文档说「照作者原文引用」,但 cd 基址会改写成 ${base}/${t}test-plan.ts:334);与 compose-review 的逐字引用契约不一致。
  • HEADING_LINE_RE 要求 # 在第 0 列,CommonMark 允许最多三个前导空格。方向安全,仅作记录。

做得好的地方testPlanGate 的非阻断契约在 composeReview 三条返回路径上都落实了,配合 diffHash 过期丢弃与 MAX_NOTES 上限,退化为沉默而非噪音;base-tree 拒绝在 ok: truebuild[] 为空时标记 available,避免把「什么都没构建」读成行为差异;hunk-survived 对文件级 inertrestatesInert 去重很到位;安全面干净——PR 正文仅进正则与 existsSyncgh 走 argv,声称的命令从不执行,base-tree 构建的是可信的 base 分支代码。

…fence backreference

Four live findings from the latest inline review round (the rest of the
round was already fixed upstream by the takeover bot - verified by
probing head behavior rather than re-reading the threads):

- base-tree's lock catch distinguishes EEXIST (a concurrent builder,
  busy) from EPERM/EROFS/ENOSPC (this run's own failure, reported as
  such, not as a busy that will never clear).
- "bun test" is bun's built-in runner, not a package-script alias: it
  runs whether or not any manifest defines test, so ruling it against
  the scripts table filed a false contradicted.
- A chained cd matches the leading-cd shape but the single-hop resolver
  joined file tokens against the FIRST directory; it now bails like the
  exotic-cd case.
- codeSpans' fence regex closes on its own marker via backreference; a
  tilde fence line inside a backtick block ended the span early and
  lines after it were lost to extraction.
@wenshao
wenshao requested a review from Copilot August 1, 2026 07:27

@tanzhenxin tanzhenxin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM!

@wenshao

wenshao commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@wenshao

wenshao commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Re-verification — 7c6b678e1

Re-ran everything from the previous pass against the new head (cdff5693b7c6b678e1, one commit, +41/−5 across base-tree.ts, test-plan.ts, test-plan.test.ts).

Fixed and verified

Fix Verified
EEXIST-only lock busy ✅ ran runBaseTree with the lock's parent directory chmod 0555: available: false, note: "could not take the base-tree build lock: EACCES: permission denied, mkdir …/wt-base.lock". No longer the misleading "another probe is building… retry when its marker appears" for a marker that would never appear.
bun test alias npmScriptOf('bun test') → null; bun run lint → 'lint', npm test → 'test', yarn test → 'test' all still rule.
chained cd bail cd a && cd packages/b && npx vitest run src/x.test.ts[]; the single-hop form is unchanged (packages/b + packages/b/src/x.test.ts).
fence backreference ✅ a ~~~ line inside a ``` block no longer truncates the span — both commands and both paths now extract.

No regressions on the two real bodies I used last time: #8215 and #8176 still find ## Reviewer Test Plan and produce byte-identical claim sets to the previous head, including #8176's four vitest paths + packages/core cd base + three npm commands + 471 tests.

Suite: cd packages/cli && npx vitest run src/commands/review src/commands/review.test.ts42 files, 1216 tests pass (was 1213 at cdff5693b; +3 from this commit). The Test Plan still claims 1161, so this remains a differs, not a contradicted — still the case the design argues for.

I also probed one regression risk in the chained-cd bail and it does not fire: cd packages/b && npx vitest run src/x.test.ts -t 'cd handling' still extracts both paths, because /(^|\s)cd\s/ is anchored on whitespace and the quote before cd blocks the match. Good.

Still open

  • The main one — per-hunk probes still have no file-kind or content eligibility filter. Unchanged at test-efficacy.ts:1635: hunkProbeInputs(worktree, base, headSha, revert, …) is still fed the unfiltered revert, while mutants still narrow with MUTANT_SOURCE_RE. Re-confirmed against the new head — both of these are still selected as hunk-probe candidates:

    classifyPath('packages/core/src/skills/bundled/review/SKILL.md') = source
    selected = ["packages/core/src/skills/bundled/review/SKILL.md @@ -10,3 +10,4 @@",
                "src/x.ts @@ -5,3 +5,5 @@"]        ← comment-only hunk
    

    Still worth closing before merge in my view: it costs a full suite run out of a cap of 6 and, when green, emits a Source: [test] Suggestion asking the author to write a test for a prose paragraph or a comment block.

  • runOneHunkProbe still does not take dependencyRoot (test-efficacy.ts:1766).

  • Stale node_modules comment still at test-efficacy.ts:1564.

  • 30-minute lock-staleness sweep still at base-tree.ts:219 (sweeps the lock, then discardWorktrees a possibly-live builder's tree).

  • TestPlanClaim.text doc vs. the cd-base rewrite, now at test-plan.ts:340.

  • HEADING_LINE_RE still requires column-0 # (test-plan.ts:124) — safe direction, note only.

All five are small; none of them blocks.

Two new, minor

  • The non-EEXIST branch is untested. base-tree.test.ts is unchanged in this commit; its two lock tests (returns BUSY…, sweeps a STALE lock…) both exercise the EEXIST path only. I verified the new branch by hand with the chmod 0555 recipe above, so it works — but this repo has a documented policy against chmod-based tests (bypassed by root, differs under CI's user), so a test here may genuinely not be worth it. Flagging so the choice is deliberate rather than an oversight.

  • The bun alias fix is slightly wider than the defect. bun test is bun's built-in runner and correctly should not be ruled against the scripts table — but dropping bun from the alias regex entirely also stops ruling bun start / bun stop / bun restart, which are script aliases. Excluding only test for bun keeps the true positive:

    const alias =
      /^(?:npm|pnpm|yarn)\s+(test|start|stop|restart)(?=\s|$)/.exec(command) ??
      /^bun\s+(start|stop|restart)(?=\s|$)/.exec(command);

    Silence-biased as-is, so purely optional — and irrelevant to this repo, which is npm-only.

中文

已针对新 head(cdff5693b7c6b678e1,单个提交,+41/−5)重跑上一轮全部验证。

已修复并验证

  • EEXIST-only 锁:把锁的父目录 chmod 0555 后调用 runBaseTree,得到 available: falsenote: "could not take the base-tree build lock: EACCES: permission denied, mkdir …",不再是「另一个探针正在构建、等它的 marker」——而那个 marker 永远不会出现。✅
  • bun testnpmScriptOf('bun test') → nullbun run lint/npm test/yarn test 仍正常裁定。✅
  • 链式 cd bail:cd a && cd packages/b && …[];单跳形式不变。✅
  • fence 反向引用:``` 块内的 ~~~ 行不再提前截断,两条命令两个路径都能抽出。✅

无回归#8215#8176 两份真实正文的抽取结果与上一轮逐字一致。

测试套件:42 文件、1216 项测试通过(cdff5693b 时为 1213,本次 +3)。Test Plan 仍声明 1161,故仍应判 differs 而非 contradicted——正是设计所论证的情形。

另外专门探了链式 cd bail 的一个回归风险:… -t 'cd handling' 不会误触发,因为 /(^|\s)cd\s/ 锚在空白上,引号挡住了匹配。✅

仍未处理

  • 主要那条:per-hunk 探针仍无文件类型/内容准入过滤。test-efficacy.ts:1635 依旧把完整的 revert 喂给 hunkProbeInputs,而 mutant 仍用 MUTANT_SOURCE_RE 收窄。在新 head 上复验:SKILL.md 的散文 hunk 与 .ts 的纯注释 hunk 依旧都被选中。个人认为值得在合并前收掉——它会花掉 cap=6 中的一次完整套件运行,且返回绿色时产出一条要求作者「为散文/注释补测试」的 Source: [test] Suggestion。
  • runOneHunkProbe 仍未接收 dependencyRoottest-efficacy.ts:1766)。
  • test-efficacy.ts:1564 的过时注释仍在。
  • base-tree.ts:219 的 30 分钟锁失效清扫仍在。
  • TestPlanClaim.text 与 cd 基址改写的不一致,现位于 test-plan.ts:340
  • HEADING_LINE_RE 仍要求 # 在第 0 列(test-plan.ts:124)——方向安全,仅记录。

以上五条都很小,均不阻断。

两条新的小意见

  • 非 EEXIST 分支没有测试base-tree.test.ts 本次未改,两个锁测试都只走 EEXIST 路径。我用上面的 chmod 0555 手法手工验证过分支可用;但本仓库对 chmod 类测试有明确的规避理由(root 可绕过、CI 用户行为不同),所以这里不加测试可能是合理的——提出来只是让这个取舍是有意的而非遗漏。
  • bun 别名的修复比缺陷略宽bun test 确实是内建 runner、不该对照 scripts 表裁定;但把 bun 整个从别名正则里去掉,也一并停止裁定 bun start/bun stop/bun restart——那些确实是脚本别名。只对 bun 排除 test 即可保留这个真阳性。方向偏沉默,纯可选,且与本仓库(纯 npm)无关。

@qwen-code-ci-bot

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

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

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

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

Verification report

PR 8215 — Deep Verification Report

Verdict: merge-ready — 69/69 scripted harness assertions passed, 0 failed; targeted gate green (42 files, 1213 passed | 3 skipped). Verified head: 7c6b678e10dceac709867e0772bc23879b428d39 (matches the metadata headRefOid). Base tip ce8eb830d (HEAD^1).

中文摘要
  • 结论merge-ready。三个新能力(test-plan 断言核对、base-tree A/B harness、per-hunk 探针)均以编译产物 dist/ 为对象、用无 mock 线束驱动验证;69/69 脚本化断言通过,0 失败;定向门禁 42 文件全绿(1213 通过 | 3 跳过)。
  • A/B 结论:两个配套修复均证明为 load-bearing——trimOutput 的 runner 汇总行抢救(head 抢救出汇总行,回退到 base 形态的对照则丢弃,见 01-ab-supporting-fixes.png);observedTestCounts 的 ANSI 剥离(head 把交织颜色码的 Tests … 1132 passed 解析为 1132,去掉剥离的对照解析为空)。ReDoS 阶梯在 2k/3k/5k/20k 字符的恶意 PR body 上耗时平坦(约 0.3ms,2k→20k 比值 1.1x),代码注释中"已消除二次回溯"的声明成立。
  • 核心契约成立test-plan 的沉默偏向契约经线束逐条验证——缺失路径/脚本判 contradicted,计数不符只判 differs、绝不判矛盾,散文不抽取,绝对路径与 owner/repoorigin/main 等 slug 不产生断言(02-testplan-contract.png,35/35)。空转检查:把 differs 规则回退为 contradicted 后,契约测试以预期断言变红(2 failed | 68 passed),证明该测试非空转(03-vacuity-differs-rule.png)。
  • findings:无阻断性 finding。一条信息性观察:PR 自身 Test Plan 声称"1161 tests",而合并后的树实测为 1216(文件数 42 完全一致)——这正是本 PR 设计中计数只判 differs 的现实例证,非缺陷。
  • 未覆盖:未做真实 merge base 的端到端构建(仅验证 availability gate 的纯路径 + 依赖项目自带 base-tree.test.ts 15 项);未跑 PR 描述的 tmux 真模型 /review(需模型与凭据,本环境无);macOS/Windows 未测(仅 Linux,与 PR 自述一致);逐 commit 归因不可达(shallow depth 2,16 commit 仅 3 个可达)。

Scope

Central claim: qwen review test-plan extracts the checkable claims (paths, npm scripts, test counts) from a PR's Test Plan and rules on them against the reviewed tree under a silence-bias contract — contradicted only for a path that is in neither the diff nor the tree, or a script no manifest defines; a count mismatch is only ever differs, never a contradiction; prose, absolute paths, and slug/ref tokens produce no claim; rulings are disclosed but never block.

Secondary claims: (1) the two supporting fixes are load-bearing — ANSI-SGR stripping in observedTestCounts, and runner-summary rescue in trimOutput; (2) the per-hunk probe splitter/selector produces self-contained, correctly-anchored, budget-honest patches; (3) base-tree's availability gate reports an unavailable base as a harness fact (available:false + note), never a throw or a finding.

Central claim — wire-oracle + A/B

All harnesses drive the compiled dist/ modules (mock-free w.r.t. the unit under test); only the PR-body fetch is injected (fetchBody), which is the network seam, not the code under test.

Cell Oracle Result
test-plan contract matrix (35 assertions) rulings on a synthetic worktree + plan: diff path → reproduces, tree-only path → reproduces, missing path → contradicted, defined script → reproduces, undefined script → contradicted, count match → reproduces, count mismatch → differs (never contradicted), no observed count → unchecked, prose/slug/absolute → no claim 35/3502-testplan-contract.png
A/B trimOutput summary rescue identical failing-suite output (summary buried in the trimmed middle) through head vs a scratch-revert control differing only by the RUNNER_SUMMARY_RE clause head rescues the summary, control drops it; cap guard holds (40k summary lines do not defeat the trim) — 01-ab-supporting-fixes.png
A/B observedTestCounts ANSI strip identical ANSI-laced Tests … 1132 passed through head vs a control with the .replace(/\x1b\[[0-9;]*m/g,'') removed head parses 1132, control returns empty01-ab-supporting-fixes.png
ReDoS ladder on extractTestPlanSection unclosed ** + 2k/3k/5k/20k spaces (untrusted PR body shape), timed 0.31 / 0.29 / 0.30 / 0.33 ms; 2k→20k ratio 1.1x — flat, no quadratic blowup
per-hunk splitDiffIntoHunks / selectHunkProbes self-contained patches, file-header-per-hunk, startLine anchored at first added line, -@@/+@@ not headers, cap counted not dropped (8→6 selected + 2 skippedForCap), mutant-overlap skip, /dev/null skip, new-test preference 18/18
base-tree availability gate no mergeBaseSha, baseFetchFailed, missing worktree, unreadable plan — each with a build seam that throws if called 7/7 available:false + explanatory note, build never invoked

Vacuity check (central test is pinned): reverting the single verdict: 'differs''contradicted' hunk in test-plan.ts turned exactly two tests red on the intended assertion — expected 'contradicted' to be 'differs' (line 801) and expected [ 'contradicted' ] to deeply equal [ 'differs' ] (line 881) — with 68 still green. Source restored afterward (git status clean). 03-vacuity-differs-rule.png.

Targeted gate: cd packages/cli && npx vitest run src/commands/review src/commands/review.test.ts42 files, 1213 passed | 3 skipped (1216), including the new test-plan.test.ts (70) and base-tree.test.ts (15). 04-review-gate.png.

Corrections

None required — the PR description's mechanism accounts matched the code as read and as driven.

Findings

No blocking findings.

  • (informational) The PR's own Test Plan states "expect all 42 files and 1161 tests to pass". The merged tree (HEAD, which includes the repeated main-branch merges) measures 42 files / 1216 tests — the file count matches exactly, the test count has drifted by 55. Per this PR's own design this is a differs, not a contradicted: the claim and the observation are about different points in the merge history. This is a live demonstration of exactly the silence-bias contract the PR exists to encode, not a defect. No action needed.

Not covered

  • End-to-end base-tree build (checking out and compiling a real merge base in a sibling worktree) was not run — it costs a full second build. Verified instead: the four cheap availability-gate paths (7/7, with a throwing build seam proving no build fires on an unavailable path) plus the project's own base-tree.test.ts (15 tests, green in the gate). The actual build-then-available:true path rests on that suite, not on an independent harness here.
  • The PR's tmux + real-model /review 8176 evidence could not be reproduced — it needs a model and credentials this sandbox does not have. The subcommand-level behavior it exercises is what the harnesses above cover directly.
  • Per-commit attribution is out of reach: the checkout is depth 2 (only the merge commit, base tip, and PR head are local), while the metadata lists 16 commits. Verified the aggregate HEAD^1..HEAD diff; per-commit claims were not individually exercised.
  • macOS / Windows not exercised (Linux only), matching the PR's own "Tested on" table.
  • The hunks/probe integration path (test-efficacy.integration.test.ts, which drives a real probe worktree + vitest) was run only as part of the green gate, not independently A/B'd; the pure splitter/selector it builds on is covered by the 18/18 wire-oracle.

Methodology

Environment: the CI verify container (node:22-bookworm), merge-ref checkout at depth 2, npm ci + npm run build already complete at HEAD. Harnesses (.mjs, kept under harness/) import the compiled modules from packages/cli/dist/src/commands/review/ and drive them against synthetic worktrees, plan JSON, and injected PR bodies — no stub of the code under test. A/B controls were built by reverting only the key hunk in a scratch copy of the compiled module (written as a sibling so relative imports resolve, then unlinked — git status and a ls _ab_*.mjs confirm none remain), so head and control differ by nothing else; the exact hunk each control represents is named in harness/harness-ab.mjs. Raw per-harness logs live in harness/*.log; image evidence in evidence/. Assertion totals in assertions.json count only the scripted checks that executed (69 across the four harnesses); the project suite's 1213 passes and the vacuity check's 2 intentional reds-on-mutated-source are reported separately and not folded into that number.

Evidence images

01-ab-supporting-fixes

02-testplan-contract

03-vacuity-differs-rule

04-review-gate

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

Qwen Code · sandboxed verification

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/cli/src/commands/review/test-efficacy.ts:1375

  • The hunk-probe restore currently rewrites only the file contents (writeFileSync(abs, original)), but git apply --reverse can also change file metadata (e.g. executable bit from old mode/new mode headers) or replace a symlink with a regular file. That can leave the probe tree mutated for subsequent probes and misattribute outcomes. Prefer restoring from HEAD via git checkout -- <file> (which restores content + mode + symlink) and fall back to the captured bytes only if checkout fails.
    // Restore by content, not by re-applying the patch forward: a forward apply
    // can fail on its own and would leave the tree neutralised for every later
    // probe, turning one bad restore into a run of false survivors. Writing the
    // saved bytes back also recreates a file the reverse patch deleted — and
    // the parent directory first: reverse-applying a `new file` hunk removes

@wenshao

wenshao commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

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

@wenshao
wenshao added this pull request to the merge queue Aug 1, 2026
Merged via the queue into main with commit e6e59fb Aug 1, 2026
246 of 255 checks passed

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

Nice work — the three-way verification split (test-plan / base-tree / per-hunk) is well-motivated and the silence-bias discipline is consistent throughout. The execution-based test strategy (real git + fake vitest) gives strong regression coverage for the worktree lifecycle and patch arithmetic.

Four non-blocking suggestions for a follow-up, all in test-plan.ts and all failing in the safe direction (toward unchecked / fewer notes rather than false findings):

  1. [Suggestion] The buildTest parse guard (try { JSON.parse } catch { null }) only catches read/syntax errors. A report that parses but is wrong-shaped (e.g. {"build": {"not": "an array"}}) crashes runTestPlan instead of degrading to unchecked. The outer handler catches it and no report is written, so the practical cost is "test-plan silently absent" rather than a wrong note — but a shape guard would make the degradation intentional rather than accidental.

  2. [Suggestion] Quote-stripping before tokenization (rest.replace(/'[^']*'/g, '')) makes the flag-value guard skip a positional path that follows a quoted flag value: -t 'covers write/edit tools' src/edit.test.ts loses the path claim. Direction is "fewer claims extracted" so no false positive, but it is a silent coverage gap for the idiomatic vitest -t form.

  3. [Suggestion] A renamed file's OLD path never enters the changed set, so a Test Plan referencing it is ruled contradicted with a note that is factually correct ("no such file") but misleading (the file was renamed, not deleted). This is the one item whose failure direction is "an extra note" rather than "a missing note" — still labeled "not a blocker", so low cost, but a git log --diff-filter=R check or a note qualifier ("possibly renamed") would be more precise.

  4. [Suggestion] defined.size === 0 emits "no package manifest could be read", but the zero can also mean "manifests were read and define zero scripts" (a repo with no workspaces field and a root package.json lacking scripts). The verdict (unchecked) is correct either way; only the note wording conflates the two.

None of these affect the verdict or the non-blocking contract. Approving.

wenshao added a commit to zhuyuy/qwen-code that referenced this pull request Aug 1, 2026
…nts, doc parity (QwenLM#8218)

* feat(review): borrowed-verification trio — test-plan check, base-tree A/B, per-hunk probes

- qwen review test-plan: rule on the PR Test Plan's checkable claims (paths,
  npm scripts, test counts) against the reviewed tree; contradictions and
  differing counts are disclosed via compose-review, never capping.
- qwen review base-tree: build the merge base in a sibling worktree so the
  verifier can A/B a comparative claim instead of reading it; swept by cleanup.
- test-efficacy: third probe kind — reverse-apply one hunk at a time and
  re-run the affected tests, attributing a still-green suite to the specific
  change nothing gates; shares the mutants' budget window, runs last.

* fix(review): survive real runner output — ANSI-laced and trimmed-away summaries

Both measured on a live /review of QwenLM#8176 with the built CLI:

- test-plan's observedTestCounts strips SGR sequences before matching; a
  color-enabled pipe interleaves them BETWEEN tokens, and the count claim
  fell to 'unchecked' with the summary right there in the report.
- build-test's trimOutput rescues runner summary lines from the omitted
  middle (like module-resolution errors): a failing suite's tail is all
  failure details and npm epilogue, which pushed the one-line summary out
  of the kept text entirely.

* fix(review): address the eight findings from live review of this branch

All measured in the review (QwenLM#8215 review comment):

- test-plan: linear-time bold-heading scan (the old pattern backtracked
  catastrophically on an unclosed ** line an untrusted PR body controls);
  a flag preceding the npm script yields no claim instead of a false
  'no package defines this script'.
- test-efficacy: a hunk probe's restore recreates the parent directory a
  reverse-applied 'new file' hunk removed (the ENOENT from finally lost
  the verdict and marked every remaining hunk inconclusive); hunks get
  their own skippedForBaseline instead of mislabeling a red baseline as
  a budget skip; splitDiffIntoHunks re-captures the file header at every
  diff --git boundary; a hunk-survived finding notes when it restates an
  inert file-level revert at hunk granularity.
- base-tree: idempotent fast path keyed on a build marker + HEAD check —
  concurrent verifier shards reuse one built tree instead of sweeping it
  out from under each other mid-A/B (a fabricated base-side difference
  with a deterministic source tag was the worst case); cost wording is
  now 'an install and a build' everywhere it was 'one extra build'.

* fix(cli): never score a hunk survived when its own test left the baseline (QwenLM#8215)

A per-hunk probe reported `survived` whenever the green baseline probes still
passed with the hunk reverted. When the hunk's own collocated test dropped out
of the baseline (a probe-tree import error collects nothing), the remaining
green probes prove only that THEY do not cover the hunk, so the verdict is now
`inconclusive` — the same dropped-test asymmetry the mutants already hold.

Also scope the hunk-survived cross-reference note to the hunk's own collocated
test, and let test-plan match a workspace-scoped run of the plan's bare command
instead of falling through to the manifest on an exact-string miss.

* fix(review): silence-bias hardening from four live review rounds of this branch

The two blocking findings, reproduced on this PR's own Test Plan:

- test-plan files no false contradicted notes: npm rulings move from a
  four-verb denylist to an allowlist (the run form + npm's script aliases —
  the ~fifty other builtins each used to become 'no package defines this
  script'); a slash token is claimed as a repo path only with evidence (an
  extension or ./ prefix), never when it is a flag's value (--repo
  owner/repo) or under the review's own temp root; HEADING_LINE_RE drops
  the same quadratic shape its bold sibling was rewritten to remove.
- base-tree gets a real mutual-exclusion lock around sweep+add+build
  (mkdirSync test-and-set; the loser returns busy instead of deleting the
  tree the winner is mid-install in), and a failed build writes a settled
  marker so later shards stop re-paying the install to relearn
  'unavailable'.

Also: Agent 7's brief now names hunk-survived and the hunks.* counters
(it is the report's only consumer, and the finding class was invisible);
hunk findings anchor at the first ADDED line instead of up to three
context lines above the change.

* feat(review): four round-2 borrowings — measured failure attribution, round ledger, richer mutants, doc parity

Shaped by a live round-2 maintainer re-verification (QwenLM#7818):

- qwen review test-delta: rerun the PR side's failed test commands on the
  built merge base and diff the failing FILE SETS — netNew is the PR's own
  failure by measurement (whatever files the diff touches), shared is
  pre-existing by measurement. Counts are never compared: a flaky suite
  fails different test names between runs of the same tree. An unfinished
  base rerun attributes nothing.
- Round ledger: the incremental review cache persists confirmed findings
  under round-scoped ids (R1-2); a high-effort re-review rules on each
  (fixed / still stands / cannot tell) and opens its report with the table,
  the way a human round-2 comment opens with 'M1 is fixed'.
- Three replacement mutation operators in test-efficacy: drop '?? fallback',
  force a comparison-bearing guard condition to 'true', drop a '+ CONST'
  term. Each survivor maps to one crisp untested-protection sentence.
  A line whose raw text and literal-blanked code view disagree yields no
  candidate — an edit index computed across the two views once spliced
  'iftrue 0)' into a guard, and a mangled mutant reads as inconclusive
  while quietly spending a cap slot.
- Quality brief: documentation-parity lens — a new user-facing surface whose
  siblings are documented is a Suggestion naming the sibling precedent; no
  documented sibling, no finding.

* fix(review): address review feedback — false-positive hardening, binary diff guard, error convention (QwenLM#8215)

* fix(review): address review feedback — base-tree availability gate, test-plan false positives, hunk-probe ranges (QwenLM#8215)

- base-tree: only stamp a base tree available when runBuildTest actually
  compiled something (ok AND npm toolchain AND a non-empty build). An
  `unsupported` handoff or an empty npm scope returns ok:true having built
  nothing; marking that tree available let an A/B read the absence of a build
  as a behavioural difference.
- cleanup: sweep the stale base-tree build lock a killed builder leaves behind.
- test-plan: read the root manifest's scripts directly so a root-only script
  survives when the root defines no build/test; bail on the inline --root=./dir
  rebasing form; stop treating a positional after an inline --flag=value as the
  flag's value; prefer a failed scoped run when ruling a bare command; anchor
  the npm script alias to a full token so `yarn test:unit` is not truncated.
- test-efficacy: exclude `\ No newline at end of file` from the startLine offset
  count; compute the mutant-overlap range from the header's new-side span so it
  no longer overshoots into a closely following hunk.

* fix(review): address review feedback — diff-header false positives, stale prompt enumeration, added-file hunk probes (QwenLM#8215)

* fix(review): address review feedback — cd-base exclusion, Test Files count guard, base-tree error handling, probe delegation (QwenLM#8215)

* fix(review): port the collocated-dropout test to the post-QwenLM#8050 runner seam

Merging main brought QwenLM#8050's Windows-portability refactor, which resolves
the probe runner through vitest/package.json's bin — a node_modules/.bin
fake is dead weight it never reads. The 8215-only collocated-dropout test
still installed the old .bin fake, so the REAL vitest ran its fixtures,
price.test.ts genuinely passed, and the hunk scored survived. The test now
overrides the fake package's vitest.mjs like every post-refactor test.

* fix(review): bound the summary rescue, apply the ATX heading rule, sweep stale build locks

The three 8215-layer findings from the latest review, fixed at this
layer (they were first patched further up the stack, where the reviewer
of THIS PR cannot see them):

- trimOutput's summary rescue is capped at 40 lines — uncapped, 40k
  lines of 'Test <n>: …' prose voided the trim entirely (measured 1.6MB
  in, 1.6MB out) and the bounded-output contract is the whole point.
- A '#' with no following whitespace is prose, not a heading (the ATX
  rule GitHub applies): 'QwenLM#8176', '#tag', an unfenced '#!/bin/bash' no
  longer end the Test Plan section mid-body; the bare-#-run crash on
  the closing scan is guarded.
- A base-tree build lock older than 30 minutes is a corpse left by a
  killed builder — swept and rebuilt instead of reporting busy for the
  rest of the review.

* fix(review): EEXIST-only lock busy, bun test alias, chained cd bail, fence backreference

Four live findings from the latest inline review round (the rest of the
round was already fixed upstream by the takeover bot - verified by
probing head behavior rather than re-reading the threads):

- base-tree's lock catch distinguishes EEXIST (a concurrent builder,
  busy) from EPERM/EROFS/ENOSPC (this run's own failure, reported as
  such, not as a busy that will never clear).
- "bun test" is bun's built-in runner, not a package-script alias: it
  runs whether or not any manifest defines test, so ruling it against
  the scripts table filed a false contradicted.
- A chained cd matches the leading-cd shape but the single-hop resolver
  joined file tokens against the FIRST directory; it now bails like the
  exotic-cd case.
- codeSpans' fence regex closes on its own marker via backreference; a
  tilde fence line inside a backtick block ended the span early and
  lines after it were lost to extraction.

* fix(review): close the ten open findings on this PR

- guard-true tested for a comparison anywhere after `if (`, including the
  then-body, so `if (ready) emit(a !== b);` admitted a mutant on the
  comparison-less condition the gate exists to exclude. It now tests the
  condition span only.
- The `survived` detail said "when it changes" for legacy DELETION
  mutants too; it now matches the operator.
- test-delta's `unparsed` required both sides to parse zero files, so a
  PR-side failure whose FAIL lines the trim scattered was silently
  dropped whenever the base rerun happened to parse. netNew/shared come
  from the PR side, so the PR side alone decides.
- failingFilesOf now matches Windows path shapes (backslashes, C:) - a
  missed parse is an unattributed failure, not a loud error.
- The replacement branch of runOneMutant (write-file -> run-probe ->
  classify) had no end-to-end test; one now drives a coalesce operator
  through the real handler and asserts the mutated line, the verdict,
  the operator-specific wording, and that the shared tree is untouched.
- Two tests were vacuous with respect to what they promised: the
  baseline-dir test never asserted the cwd (its helper swallowed the
  argument - fixed at the helper), and the one-candidate-per-line test
  used an input that never triggered the replacement path, so the
  `continue` under test was not load-bearing.
- Reattached the orphaned selectMutants JSDoc; reworded the SKILL line.

* fix(review): the test helper's cwd parameter is required, not optional

CI's `tsc --build` failed on test-delta.test.ts: the exec seam always
passes a cwd, but the helper's signature marked it optional, so pushing
it into a string[] was `string | undefined`.

Missed locally because vitest runs through esbuild, which strips types
without checking them - the suite was green while the build was red. The
gate to run before pushing a type-level change is `npx tsc --build`, not
the test suite.

* fix(review): a base rerun that could not RUN attributes nothing

Two Criticals from the latest review, both reachable on the brief's own
happy path:

- baseUnusable covered only timeouts. Every other way the base side can
  fail to run - an unbuilt base tree, a missing install, a workspace the
  PR ADDED (npm test --workspace cannot resolve on base), an ENOBUFS
  truncation - exits non-zero with zero FAIL lines, which this code read
  as a green base. Every PR-side failure then became netNew: the
  strongest evidence the command emits, manufactured from a base that
  never ran a test. It now attributes nothing and says why.
- Timeout detection was the weaker substring form the sibling explicitly
  rejects; an external SIGTERM (container stop, cancelled job) set
  neither an ETIMEDOUT message nor an exit code and fed straight into
  the above. build-test now exports spawnTimedOut and test-delta asks
  the same question rather than re-deriving it.

Also: the base output is trimmed (it precedes the verdict fields in the
report the agent reads, so an untrimmed megabyte truncates exactly what
the command produces); the guard-true gate no longer reads an arrow
function's => as a comparison (every predicate guard was a candidate -
the if (ready) noise the gate exists to exclude); the term-drop message
no longer calls a string concatenation a reserve term; the unparsed note
describes its own PR-side-only condition; and the ledger's Step 6/Step 8
now agree that a still-standing finding keeps its id.

* fix(review): compare failing files by a normalised, project-keyed identity

Critical: the two sides run in DIFFERENT roots (the PR worktree and the
base tree), and netNew/shared compared the parsed paths verbatim - so an
absolute-path runner turned every pre-existing failure into a fabricated
Critical, with the authority of a measurement behind it. Paths are now
normalised against each run's own root (and backslashes to /, so a
Windows path compares with its POSIX-printed twin), which is why
test-delta gained --pr-worktree.

The identity also keeps the vitest project token: dropping it collapsed
same-named test files across workspaces, so a PR-caused failure in one
package could read as pre-existing because another package has a file by
the same name - the worse failure direction.

Also from the same review, all of them reachable on the brief's own path:

- The base rerun now inherits build-test's stdio: ['ignore','pipe','pipe']
  ("a build that asks a question is a build that hangs until the
  deadline") and its trimOutput, which matters because entries[].base
  precedes the verdict fields in the report the agent reads.
- The brief gates on base-tree's `available`, not just its `path`: a tree
  that was created but did not build populates path too, and measuring
  against it turns an infrastructure failure into Criticals.
- A programmatic caller omitting `timeout` no longer sends NaN into
  spawnSync.
- MutantCandidate is a discriminated union, so an operator without its
  replacement line - which would delete a line while reporting "with its
  ?? fallback dropped" - is unrepresentable.
- The comparison class no longer requires a trailing space (if (a<b) is
  the same guard, just unformatted) and matches a brace-less else if.
- DeltaEntry.unparsed's doc now describes the PR-side-only condition it
  actually implements.

* fix(review): restore the whole-command budget, keep generics out of guard-true

Round-1 findings from a fresh review of this PR:

- test-delta had no aggregate deadline: --timeout is PER command and
  defaults to 300s, so three failed commands is 900s against Agent 7's
  600s ceiling - killed with NO report at all, discarding the base-tree
  install and build just paid for. TOTAL_BUDGET_MS mirrors the one
  test-efficacy reserves; commands it cannot fit are disclosed.
- guard-true matched generic calls: `if (isRecord<string>(v))` produced a
  mutant, and a type-guard predicate is exactly the `if (ready)` shape
  whose survivors the gate calls noise. The trailing \s is required, not
  an accidental asymmetry with [!=]== - telling `a<b` from `fn<T>(x)`
  needs a parser, and the gate is silence-biased by design.
- --pr-worktree had no contract test, and its failure mode is the worst
  here: arriving undefined, root stripping silently stops and every
  pre-existing failure becomes a fabricated netNew. The new test feeds
  parseSync's output straight into runTestDelta and asserts an
  attribution only reachable when both roots were stripped (verified
  red against the snake_case field shape that shipped once already).
- Merged the two consecutive doc comments on prWorktree.

* fix(review): a budget-shortened deadline is not the same fact as a slow rerun

Round-2 finding on the budget just restored: `Math.min(perCommandMs,
remaining)` can hand a rerun far less than --timeout, and if it dies
there the note said only "timed out - infrastructure, not evidence".
True, but it sends the reader hunting a hang that is really an exhausted
budget - and unlike a real timeout, a rerun with budget to spare would
still measure it. The note now names those commands separately and says
so. Verified red against removing the tracking line.

* fix(review): restore the replacement sub-cap, stranded on a downstream branch

Round-3 finding, and the third instance of one class: an 8218-layer fix
committed on the 8261 branch, four PRs above the code it belongs to.

Measured over 40 real commits, the replacement operators produce ~24x
the deletion pool (215 vs 9 candidates; guard-true drives it). Every
mutant run drains the same window hunk probes draw from LAST, so
uncapped, most diffs with any replacement candidates leave hunk probing
zero runs - the hunk-survived finding class silently stops firing and
nothing says so. Three slots, and what the sub-cap drops is counted in
skippedForCap rather than lost.

Also swept the other direction: diffed every review file against its
8261 copy to confirm nothing else 8218-layer is stranded up there. The
remaining divergence is 8261's own (the positive control, its lenses).

* docs(review): complete the "delta cannot rule" enumeration in both places

Round-4 finding. The brief and SKILL.md each listed three cases where
test-delta attributes nothing - unparsed, timed-out base, no merge base -
but the code has five: the later rounds of this PR added "a base rerun
that failed without naming any failing file" (it did not measure the
base) and "a command the whole-command budget could not fit". Two
enumerations of the same set with different membership, in the two
places an agent reads.

That is the sibling-enumeration lesson this skill teaches, applied to
its own prose for the second time: the fix is not just adding the
missing members but saying that the report names each case with its own
reason rather than folding them into one.

* docs(review): the rationale named only one of the ways base goes unmeasured

Third and last copy of the enumeration the previous commit fixed. The
bullet's headline already generalised - "base attributes nothing it did
not finish" - but its body named only the timeout, so a reader learning
the contract from the rationale would conclude timeouts are the only
unusable case. Name the set, and say why the report keeps the reasons
apart: "we could not measure" and "we measured nothing" are different
facts to the author, and only one of them is about their PR.

* fix(review): a file-count label stops counting at the end of its line

`Test Files  45 passed` filing its 45 as a differing TEST count was
fixed once, with a lookbehind on the bare-count pattern. That only ever
rejected the all-green shape. The moment any file fails, the runner
prints `Test Files  1 failed | 44 passed (45)`, the label is no longer
adjacent to the number, and 44 comes through as a test count - so the
note reads "claimed 44, observed 1323" on exactly the runs whose summary
someone would paste. Adjacency was the wrong invariant; the line is.

Masking from the label to end-of-line is distance-independent and picks
up jest's `Test Suites: 1 failed, 44 passed, 45 total` at no cost. The
label keeps its `Test` word on purpose: a first cut matched a bare
`files` and blanked the line in "expect all four files and 471 tests to
pass", silencing a real claim. An existing test caught that, which is
the argument for the rule being as narrow as it is - anything that
suppresses claims is worth exactly its narrowness.

Also drops the now-dead lookbehind, which a reviewer had separately (and
wrongly) called a JS syntax error; variable-length lookbehind is legal
in V8, and the module parsed fine. It goes because the line rule
subsumes it, not because it was broken.

* fix(review): parse the base rerun before its output is trimmed

`trimOutput` rescues module errors and runner summaries out of the
omitted middle, not the per-file FAIL lines this command reads. A base
suite whose failure section overruns the tail budget therefore lost
failing files into the gap - and a SHORT base set is the dangerous
direction, because netNew is the PR side minus the base side. Every file
the trim hid came back as a Critical attributed to this PR by
"measurement". Parse the raw text, report the bounded one.

The PR side cannot be fixed here: it is read out of build-test's stored
output, trimmed before this command existed. That loss runs the other
way - it understates `shared`, never invents a netNew - so the entry
carries `prTruncated` and the note says the list may be partial. A
silence-biased gap is still a gap the author should hear about.

Also names both selection caps in the mutant-skip diagnostic. The count
accumulates replacement sub-cap drops, and with 2 deletions and 6
replacements the total is exactly MAX_MUTANTS: the main cap never fires,
yet 3 are dropped, and the message sent the reader looking for a pool of
11 candidates that never existed.

* fix(review): rerun only the command shapes build-test emits

This command reads a report off disk and then executes the strings in it
with `shell: true`, in the base worktree. Nothing else in the pipeline
re-executes a value it read back from a file, so nothing else has to
care where that value came from - this does, and the provenance is worse
than it looks: the command is `npm test --workspace="<dir>"`, the
workspace token is a directory, and a directory is a name a pull request
chooses. `packages/x";curl …|sh;"` is a legal path in git and on Linux
and it round-trips through the report into a shell.

Restricting to the emitter's own grammar costs nothing real, since that
grammar is the two shapes build-test produces. A command outside it is
skipped and disclosed, the same treatment everything else this command
cannot do already gets, so a future shape degrades to "judge it by the
diff" rather than to arbitrary execution.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
pull Bot pushed a commit to mcx/qwen-code that referenced this pull request Aug 1, 2026
…rification lenses (QwenLM#8225)

* feat(review): borrowed-verification trio — test-plan check, base-tree A/B, per-hunk probes

- qwen review test-plan: rule on the PR Test Plan's checkable claims (paths,
  npm scripts, test counts) against the reviewed tree; contradictions and
  differing counts are disclosed via compose-review, never capping.
- qwen review base-tree: build the merge base in a sibling worktree so the
  verifier can A/B a comparative claim instead of reading it; swept by cleanup.
- test-efficacy: third probe kind — reverse-apply one hunk at a time and
  re-run the affected tests, attributing a still-green suite to the specific
  change nothing gates; shares the mutants' budget window, runs last.

* fix(review): survive real runner output — ANSI-laced and trimmed-away summaries

Both measured on a live /review of QwenLM#8176 with the built CLI:

- test-plan's observedTestCounts strips SGR sequences before matching; a
  color-enabled pipe interleaves them BETWEEN tokens, and the count claim
  fell to 'unchecked' with the summary right there in the report.
- build-test's trimOutput rescues runner summary lines from the omitted
  middle (like module-resolution errors): a failing suite's tail is all
  failure details and npm epilogue, which pushed the one-line summary out
  of the kept text entirely.

* fix(review): address the eight findings from live review of this branch

All measured in the review (QwenLM#8215 review comment):

- test-plan: linear-time bold-heading scan (the old pattern backtracked
  catastrophically on an unclosed ** line an untrusted PR body controls);
  a flag preceding the npm script yields no claim instead of a false
  'no package defines this script'.
- test-efficacy: a hunk probe's restore recreates the parent directory a
  reverse-applied 'new file' hunk removed (the ENOENT from finally lost
  the verdict and marked every remaining hunk inconclusive); hunks get
  their own skippedForBaseline instead of mislabeling a red baseline as
  a budget skip; splitDiffIntoHunks re-captures the file header at every
  diff --git boundary; a hunk-survived finding notes when it restates an
  inert file-level revert at hunk granularity.
- base-tree: idempotent fast path keyed on a build marker + HEAD check —
  concurrent verifier shards reuse one built tree instead of sweeping it
  out from under each other mid-A/B (a fabricated base-side difference
  with a deterministic source tag was the worst case); cost wording is
  now 'an install and a build' everywhere it was 'one extra build'.

* fix(cli): never score a hunk survived when its own test left the baseline (QwenLM#8215)

A per-hunk probe reported `survived` whenever the green baseline probes still
passed with the hunk reverted. When the hunk's own collocated test dropped out
of the baseline (a probe-tree import error collects nothing), the remaining
green probes prove only that THEY do not cover the hunk, so the verdict is now
`inconclusive` — the same dropped-test asymmetry the mutants already hold.

Also scope the hunk-survived cross-reference note to the hunk's own collocated
test, and let test-plan match a workspace-scoped run of the plan's bare command
instead of falling through to the manifest on an exact-string miss.

* fix(review): silence-bias hardening from four live review rounds of this branch

The two blocking findings, reproduced on this PR's own Test Plan:

- test-plan files no false contradicted notes: npm rulings move from a
  four-verb denylist to an allowlist (the run form + npm's script aliases —
  the ~fifty other builtins each used to become 'no package defines this
  script'); a slash token is claimed as a repo path only with evidence (an
  extension or ./ prefix), never when it is a flag's value (--repo
  owner/repo) or under the review's own temp root; HEADING_LINE_RE drops
  the same quadratic shape its bold sibling was rewritten to remove.
- base-tree gets a real mutual-exclusion lock around sweep+add+build
  (mkdirSync test-and-set; the loser returns busy instead of deleting the
  tree the winner is mid-install in), and a failed build writes a settled
  marker so later shards stop re-paying the install to relearn
  'unavailable'.

Also: Agent 7's brief now names hunk-survived and the hunks.* counters
(it is the report's only consumer, and the finding class was invisible);
hunk findings anchor at the first ADDED line instead of up to three
context lines above the change.

* feat(review): four round-2 borrowings — measured failure attribution, round ledger, richer mutants, doc parity

Shaped by a live round-2 maintainer re-verification (QwenLM#7818):

- qwen review test-delta: rerun the PR side's failed test commands on the
  built merge base and diff the failing FILE SETS — netNew is the PR's own
  failure by measurement (whatever files the diff touches), shared is
  pre-existing by measurement. Counts are never compared: a flaky suite
  fails different test names between runs of the same tree. An unfinished
  base rerun attributes nothing.
- Round ledger: the incremental review cache persists confirmed findings
  under round-scoped ids (R1-2); a high-effort re-review rules on each
  (fixed / still stands / cannot tell) and opens its report with the table,
  the way a human round-2 comment opens with 'M1 is fixed'.
- Three replacement mutation operators in test-efficacy: drop '?? fallback',
  force a comparison-bearing guard condition to 'true', drop a '+ CONST'
  term. Each survivor maps to one crisp untested-protection sentence.
  A line whose raw text and literal-blanked code view disagree yields no
  candidate — an edit index computed across the two views once spliced
  'iftrue 0)' into a guard, and a mangled mutant reads as inconclusive
  while quietly spending a cap slot.
- Quality brief: documentation-parity lens — a new user-facing surface whose
  siblings are documented is a Suggestion naming the sibling precedent; no
  documented sibling, no finding.

* feat(review): render adjudication, workflow step extraction, three verification lenses

Shaped by a live sanitizer-PR verification (QwenLM#8147):

- Render-adjudication capability (opt-in): with QWEN_REVIEW_SCRATCH_REPO
  set, the verifier may post a minimal payload to that user-designated repo
  and rule on GitHub's own rendered HTML — the measured case being an
  @ -> &#64; defusal every local reading called sound while GitHub's real
  renderer registered the mention and fired the notification. Absent the
  setting, rendering claims honestly cap at low confidence / cannot tell.
  Step 7's write ban names the carve-out explicitly.
- qwen review extract-step: lift one workflow step's run: script verbatim
  into an executable, with env (as comments, never half-substituted
  exports), every ${{ }} site listed unevaluated, and a heuristic invoked-
  command list as the stubbing starting point. With base-tree, both arms of
  a by-hand workflow A/B become two invocations. yaml declared as a cli
  dependency (previously resolved only via hoisting).
- Three brief lenses: a borrowed protection idiom missing what made it work
  at home (the code ancestor did the protecting; only the entity was
  copied); a second parser for an authoritatively-parsed format is a
  divergence hunt; tests that pin the mechanism instead of the effect, and
  oracles that mirror the implementation's own model.

* feat(review): sibling-entrance discipline for the fixed verdict

From round 6 of the live sanitizer verification (QwenLM#8147):
the fix closed the fence-shaped entrance into a raw-HTML block, and the
code-span entrance beside it — same divergence, adjacent syntax — stayed
open. A re-check that tests only the reported input rules 'fixed' over a
hole one backtick away.

Both fixed-verdict sites in SKILL Step 6 (the open-Criticals re-check and
the round ledger) now require enumerating a divergence-class defect's
sibling entrances before ruling fixed; a still-open sibling is a NEW
finding, never a reason to withhold the original's fixed — the two
rulings stay separate so the second hole cannot ship unreviewed.

* feat(review): three measured-verification lenses from live rounds 8037/8005

- Threshold-boundary scan: when a fix is a ratio/length guard, hold the
  issue's own variables fixed and binary-search the boundary where the
  behaviour flips; put the number next to what the issue reports. Live
  case: a prose-ratio guard covered the edit/write_file half of its issue
  and silently declined the run_shell_command half (~473-char boundary).
- Delimiter self-injection named as the first parser-differential probe:
  a no-escaping extractor fed its own close tag truncates silently.
- Shared-gate state enumeration: a deliberate-design defence extends only
  to the states it argues — an input-hold argued for 'active' silently
  froze three idle states sharing the same gate. The sibling-entrance
  rule, applied to a state machine instead of a syntax.

* fix(review): address review feedback — false-positive hardening, binary diff guard, error convention (QwenLM#8215)

* fix(review): address review feedback — base-tree availability gate, test-plan false positives, hunk-probe ranges (QwenLM#8215)

- base-tree: only stamp a base tree available when runBuildTest actually
  compiled something (ok AND npm toolchain AND a non-empty build). An
  `unsupported` handoff or an empty npm scope returns ok:true having built
  nothing; marking that tree available let an A/B read the absence of a build
  as a behavioural difference.
- cleanup: sweep the stale base-tree build lock a killed builder leaves behind.
- test-plan: read the root manifest's scripts directly so a root-only script
  survives when the root defines no build/test; bail on the inline --root=./dir
  rebasing form; stop treating a positional after an inline --flag=value as the
  flag's value; prefer a failed scoped run when ruling a bare command; anchor
  the npm script alias to a full token so `yarn test:unit` is not truncated.
- test-efficacy: exclude `\ No newline at end of file` from the startLine offset
  count; compute the mutant-overlap range from the header's new-side span so it
  no longer overshoots into a closely following hunk.

* fix(review): address review feedback — diff-header false positives, stale prompt enumeration, added-file hunk probes (QwenLM#8215)

* fix(review): address review feedback — cd-base exclusion, Test Files count guard, base-tree error handling, probe delegation (QwenLM#8215)

* fix(review): port the collocated-dropout test to the post-QwenLM#8050 runner seam

Merging main brought QwenLM#8050's Windows-portability refactor, which resolves
the probe runner through vitest/package.json's bin — a node_modules/.bin
fake is dead weight it never reads. The 8215-only collocated-dropout test
still installed the old .bin fake, so the REAL vitest ran its fixtures,
price.test.ts genuinely passed, and the hunk scored survived. The test now
overrides the fake package's vitest.mjs like every post-refactor test.

* fix(review): bound the summary rescue, apply the ATX heading rule, sweep stale build locks

The three 8215-layer findings from the latest review, fixed at this
layer (they were first patched further up the stack, where the reviewer
of THIS PR cannot see them):

- trimOutput's summary rescue is capped at 40 lines — uncapped, 40k
  lines of 'Test <n>: …' prose voided the trim entirely (measured 1.6MB
  in, 1.6MB out) and the bounded-output contract is the whole point.
- A '#' with no following whitespace is prose, not a heading (the ATX
  rule GitHub applies): 'QwenLM#8176', '#tag', an unfenced '#!/bin/bash' no
  longer end the Test Plan section mid-body; the bare-#-run crash on
  the closing scan is guarded.
- A base-tree build lock older than 30 minutes is a corpse left by a
  killed builder — swept and rebuilt instead of reporting busy for the
  rest of the review.

* fix(review): EEXIST-only lock busy, bun test alias, chained cd bail, fence backreference

Four live findings from the latest inline review round (the rest of the
round was already fixed upstream by the takeover bot - verified by
probing head behavior rather than re-reading the threads):

- base-tree's lock catch distinguishes EEXIST (a concurrent builder,
  busy) from EPERM/EROFS/ENOSPC (this run's own failure, reported as
  such, not as a busy that will never clear).
- "bun test" is bun's built-in runner, not a package-script alias: it
  runs whether or not any manifest defines test, so ruling it against
  the scripts table filed a false contradicted.
- A chained cd matches the leading-cd shape but the single-hop resolver
  joined file tokens against the FIRST directory; it now bails like the
  exotic-cd case.
- codeSpans' fence regex closes on its own marker via backreference; a
  tilde fence line inside a backtick block ended the span early and
  lines after it were lost to extraction.

* fix(review): close the ten open findings on this PR

- guard-true tested for a comparison anywhere after `if (`, including the
  then-body, so `if (ready) emit(a !== b);` admitted a mutant on the
  comparison-less condition the gate exists to exclude. It now tests the
  condition span only.
- The `survived` detail said "when it changes" for legacy DELETION
  mutants too; it now matches the operator.
- test-delta's `unparsed` required both sides to parse zero files, so a
  PR-side failure whose FAIL lines the trim scattered was silently
  dropped whenever the base rerun happened to parse. netNew/shared come
  from the PR side, so the PR side alone decides.
- failingFilesOf now matches Windows path shapes (backslashes, C:) - a
  missed parse is an unattributed failure, not a loud error.
- The replacement branch of runOneMutant (write-file -> run-probe ->
  classify) had no end-to-end test; one now drives a coalesce operator
  through the real handler and asserts the mutated line, the verdict,
  the operator-specific wording, and that the shared tree is untouched.
- Two tests were vacuous with respect to what they promised: the
  baseline-dir test never asserted the cwd (its helper swallowed the
  argument - fixed at the helper), and the one-candidate-per-line test
  used an input that never triggered the replacement path, so the
  `continue` under test was not load-bearing.
- Reattached the orphaned selectMutants JSDoc; reworded the SKILL line.

* fix(review): the test helper's cwd parameter is required, not optional

CI's `tsc --build` failed on test-delta.test.ts: the exec seam always
passes a cwd, but the helper's signature marked it optional, so pushing
it into a string[] was `string | undefined`.

Missed locally because vitest runs through esbuild, which strips types
without checking them - the suite was green while the build was red. The
gate to run before pushing a type-level change is `npx tsc --build`, not
the test suite.

* fix(review): a base rerun that could not RUN attributes nothing

Two Criticals from the latest review, both reachable on the brief's own
happy path:

- baseUnusable covered only timeouts. Every other way the base side can
  fail to run - an unbuilt base tree, a missing install, a workspace the
  PR ADDED (npm test --workspace cannot resolve on base), an ENOBUFS
  truncation - exits non-zero with zero FAIL lines, which this code read
  as a green base. Every PR-side failure then became netNew: the
  strongest evidence the command emits, manufactured from a base that
  never ran a test. It now attributes nothing and says why.
- Timeout detection was the weaker substring form the sibling explicitly
  rejects; an external SIGTERM (container stop, cancelled job) set
  neither an ETIMEDOUT message nor an exit code and fed straight into
  the above. build-test now exports spawnTimedOut and test-delta asks
  the same question rather than re-deriving it.

Also: the base output is trimmed (it precedes the verdict fields in the
report the agent reads, so an untrimmed megabyte truncates exactly what
the command produces); the guard-true gate no longer reads an arrow
function's => as a comparison (every predicate guard was a candidate -
the if (ready) noise the gate exists to exclude); the term-drop message
no longer calls a string concatenation a reserve term; the unparsed note
describes its own PR-side-only condition; and the ledger's Step 6/Step 8
now agree that a still-standing finding keeps its id.

* fix(review): extract-step resolves all three env/defaults levels, and comments every env line

Two silent-wrongness defects in a command whose whole value is fidelity.

`env:`, `shell:` and `working-directory:` are three-level settings on GitHub —
workflow, job, step, nearest wins — and only the step level appears in the
step's own text. Reading step-level alone reproduced by machine the exact
transcription error this command exists to remove: measured, a step under a
job-level `NODE_ENV: production` and a workflow-level `GLOBAL_FLAG` extracted
with `env: { LOCAL: '1' }` and `workingDirectory: undefined`, so the emitted
script ran with both unset and nothing said so. Not a contrived shape: this
repo carries workflow-level `env:` in 7 workflows, job-level `env:` in 10, and
job-level `defaults.run` in qwen-triage.yml — the workflow the command's own
test plan names. The three levels now merge with the runner's precedence, and
`envSources` records which level each key came from, so an inherited value is
visible rather than indistinguishable from the step's own.

The env block was commented per ENTRY, not per LINE. A YAML block scalar
(qwen-autofix.yml's `SETTINGS_JSON: |-`) reaches the header as a multi-line
string, so its continuation lines landed in command position — and under the
`set -e` the header itself emits, the extracted step died in its own preamble
before its `run:` body ran. Every line is commented now.

Tests pin the effect, not the mechanism: `executableLines()` asserts nothing
but the `run:` body ever reaches command position, plus a `bash -n` parse
check. Verified to flip — all five new assertions fail against the pre-fix
implementation (`{ LOCAL: '1' }`, `undefined` working directory, three
executable lines instead of one, and a real `bash -n` syntax error).

* fix(review): compare failing files by a normalised, project-keyed identity

Critical: the two sides run in DIFFERENT roots (the PR worktree and the
base tree), and netNew/shared compared the parsed paths verbatim - so an
absolute-path runner turned every pre-existing failure into a fabricated
Critical, with the authority of a measurement behind it. Paths are now
normalised against each run's own root (and backslashes to /, so a
Windows path compares with its POSIX-printed twin), which is why
test-delta gained --pr-worktree.

The identity also keeps the vitest project token: dropping it collapsed
same-named test files across workspaces, so a PR-caused failure in one
package could read as pre-existing because another package has a file by
the same name - the worse failure direction.

Also from the same review, all of them reachable on the brief's own path:

- The base rerun now inherits build-test's stdio: ['ignore','pipe','pipe']
  ("a build that asks a question is a build that hangs until the
  deadline") and its trimOutput, which matters because entries[].base
  precedes the verdict fields in the report the agent reads.
- The brief gates on base-tree's `available`, not just its `path`: a tree
  that was created but did not build populates path too, and measuring
  against it turns an infrastructure failure into Criticals.
- A programmatic caller omitting `timeout` no longer sends NaN into
  spawnSync.
- MutantCandidate is a discriminated union, so an operator without its
  replacement line - which would delete a line while reporting "with its
  ?? fallback dropped" - is unrepresentable.
- The comparison class no longer requires a trailing space (if (a<b) is
  the same guard, just unformatted) and matches a brace-less else if.
- DeltaEntry.unparsed's doc now describes the PR-side-only condition it
  actually implements.

* fix(review): restore the whole-command budget, keep generics out of guard-true

Round-1 findings from a fresh review of this PR:

- test-delta had no aggregate deadline: --timeout is PER command and
  defaults to 300s, so three failed commands is 900s against Agent 7's
  600s ceiling - killed with NO report at all, discarding the base-tree
  install and build just paid for. TOTAL_BUDGET_MS mirrors the one
  test-efficacy reserves; commands it cannot fit are disclosed.
- guard-true matched generic calls: `if (isRecord<string>(v))` produced a
  mutant, and a type-guard predicate is exactly the `if (ready)` shape
  whose survivors the gate calls noise. The trailing \s is required, not
  an accidental asymmetry with [!=]== - telling `a<b` from `fn<T>(x)`
  needs a parser, and the gate is silence-biased by design.
- --pr-worktree had no contract test, and its failure mode is the worst
  here: arriving undefined, root stripping silently stops and every
  pre-existing failure becomes a fabricated netNew. The new test feeds
  parseSync's output straight into runTestDelta and asserts an
  attribution only reachable when both roots were stripped (verified
  red against the snake_case field shape that shipped once already).
- Merged the two consecutive doc comments on prWorktree.

* fix(review): a budget-shortened deadline is not the same fact as a slow rerun

Round-2 finding on the budget just restored: `Math.min(perCommandMs,
remaining)` can hand a rerun far less than --timeout, and if it dies
there the note said only "timed out - infrastructure, not evidence".
True, but it sends the reader hunting a hang that is really an exhausted
budget - and unlike a real timeout, a rerun with budget to spare would
still measure it. The note now names those commands separately and says
so. Verified red against removing the tracking line.

* fix(review): brace-tolerant stub list, pipefail fidelity, and extract-step in the briefs

Round-3 findings on this PR, fixed.

`expressionsOf` matched `[^}]*`, so any expression containing a brace —
`format('refs/pull/{0}/head', …)`, `fromJSON('{"a":1}')` — was not mis-listed
but DROPPED. A stub list reads as "these are all the values to supply", so a
silent omission is a value that never gets stubbed. It now scans forward to the
closing `}}`, and reports nothing for an unterminated site rather than
swallowing the rest of the text.

Declaring `shell: bash` is not the runner's default `bash`. The default is
`bash -e {0}`; a declared `bash` (at any level) is
`bash --noprofile --norc -eo pipefail {0}`, and a pipeline whose middle stage
fails aborts under one and not the other. The header now carries `set -eo
pipefail` or `set -e` accordingly — 163 of this repo's 434 `run:` steps are
under a declared bash and were getting the weaker one. A `shell:` value is also
a command template (`perl {0}`), so only its first word goes in the shebang and
the whole template is recorded beside it.

`extract-step` was registered on the CLI and mentioned in DESIGN.md, and
nowhere in SKILL.md or the agent briefs — the runtime prompts. The capability
was unreachable by the agents it was written for. The verifier's brief now
carries it next to the A/B paragraph it composes with, and Step 4 summarises it.

Also: env ordered nearest-first (measured on qwen-autofix.yml:route:0, merge
order put 20 inherited entries ahead of the step's own 26 in a 49-line header);
a valueless `FOO:` renders as the empty string, not `"null"`, and a non-scalar
as JSON rather than `[object Object]`; a missing file no longer reports as a
parse failure; DESIGN.md's lens count matches its list.

The test oracle is rebuilt around the property instead of a filter: the file is
the header plus the body verbatim, and every line before the body is a comment
or a directive the test names. The old helper dropped `set -e` unconditionally,
so it could not tell the header's from one the body legitimately contains — and
would have gone green on a header that leaked exactly that line. 434 real `run:`
steps swept: 0 non-verbatim bodies, 0 live header lines, 0 missed expression
sites, 0 out-of-order env, 0 `bash -n` failures.

* fix(review): restore the replacement sub-cap, stranded on a downstream branch

Round-3 finding, and the third instance of one class: an 8218-layer fix
committed on the 8261 branch, four PRs above the code it belongs to.

Measured over 40 real commits, the replacement operators produce ~24x
the deletion pool (215 vs 9 candidates; guard-true drives it). Every
mutant run drains the same window hunk probes draw from LAST, so
uncapped, most diffs with any replacement candidates leave hunk probing
zero runs - the hunk-survived finding class silently stops firing and
nothing says so. Three slots, and what the sub-cap drops is counted in
skippedForCap rather than lost.

Also swept the other direction: diffed every review file against its
8261 copy to confirm nothing else 8218-layer is stranded up there. The
remaining divergence is 8261's own (the positive control, its lenses).

* docs(review): complete the "delta cannot rule" enumeration in both places

Round-4 finding. The brief and SKILL.md each listed three cases where
test-delta attributes nothing - unparsed, timed-out base, no merge base -
but the code has five: the later rounds of this PR added "a base rerun
that failed without naming any failing file" (it did not measure the
base) and "a command the whole-command budget could not fit". Two
enumerations of the same set with different membership, in the two
places an agent reads.

That is the sibling-enumeration lesson this skill teaches, applied to
its own prose for the second time: the fix is not just adding the
missing members but saying that the report names each case with its own
reason rather than folding them into one.

* fix(review): the invokes list was mostly prose, not commands

Round-4 finding on this PR. `invokes` is documented as a heuristic starting
point, and imprecision is fine — but measured over this repo's 434 real `run:`
steps it was reporting 435 distinct "commands", 267 of them appearing exactly
once, with a worst case of 63 entries made up of words like `CI`, `Evidence`,
`PR` and `and`. A list that size, mostly prose, is not a starting point.

Three causes, each measured:

- A `${{ … }}` expression is not shell, and it routinely contains `||`.
  Splitting on that as a pipeline separator reported both operands as commands
  (`matrix.arch`, `github.event.inputs.version`). Expressions are now masked to
  an opaque token before the split; one sitting in command position contributes
  nothing, which is honest — what it expands to is unknown here by design.
- A heredoc body is input to a command, not a list of them. Its lines were
  scanned as commands, terminator included. 12 steps in this repo carry one.
- The `name=value` skip stepped over the prefix and took the NEXT word as the
  command — but for a quoted value with spaces that word is inside the value:
  `EVIDENCE_SECTION=$'### Evidence images'` reported `Evidence`. Quoted spans
  are now blanked out, with the quote carried across lines so a multi-line
  string's continuation lines are data too. Command substitutions are read
  first, so `body="$(sanitize < "$REPORT")"` still reports `sanitize`.

A `#` preceded by whitespace ends the live part of a line, so an apostrophe in
a trailing comment cannot open a span and eat the rest of the script.

Measured after: 435 distinct commands to 187, singletons 267 to 104, worst case
63 to 27 — and the worst case is now real commands (`awk cat chmod curl git jq
mktemp pkill tar tee timeout`) plus the script's own shell functions. Also
added the builtins a stub could not intercept anyway (`eval`, `exec`, `source`,
`unset`, `command`, …) to the keyword set.

434 steps re-swept: 0 non-verbatim bodies, 0 live header lines, 0 missed
expression sites, 0 out-of-order env, 0 `bash -n` failures.

* docs(review): the rationale named only one of the ways base goes unmeasured

Third and last copy of the enumeration the previous commit fixed. The
bullet's headline already generalised - "base attributes nothing it did
not finish" - but its body named only the timeout, so a reader learning
the contract from the rationale would conclude timeouts are the only
unusable case. Name the set, and say why the report keeps the reasons
apart: "we could not measure" and "we measured nothing" are different
facts to the author, and only one of them is about their PR.

* fix(review): a quoted `<<EOF` is not a heredoc, and a continued line is one command

Round-5 findings, both in the previous commit's own scanner.

A heredoc opener was matched over the whole line, so one inside a string
started heredoc mode: `echo "write <<EOF for a heredoc"` made every later line
wait for a terminator that never arrives. The failure is not a missing entry
but a missing REST — measured, a three-line script returned `[]` instead of
`[curl, jq]`, empty and entirely plausible. Opener detection moved inside the
quote walk, where it only fires outside quotes; the quoted forms (`<<'EOF'`)
are consumed by the match, so their quotes never open a span either.

A backslash-continued command was scanned as several lines, which puts the next
ARGUMENT in command position — this is where `apt-get install -y \` /
`  libx11-dev` reported the package as an invoked command. Continuations are
now joined into one logical line before scanning.

Measured after: 187 distinct commands to 185, and the singleton tail is now
dominated by real ones — PowerShell cmdlets, macOS tooling, and the scripts'
own shell functions. 434 real steps re-swept: 0 non-verbatim bodies, 0 live
header lines, 0 missed expression sites, 0 out-of-order env, 0 `bash -n`
failures.

* fix(review): case labels, a second heredoc, and an expression in command position

Round-6 findings, from running the scanner against adversarial shapes rather
than reading it again. Two are UNDER-reports, which is the worse direction: a
command missing from the list is a stub the verifier never writes, so the
extraction reaches the real network.

- A `case` pattern label stopped the scan on its own line: `blocked) gh api x`
  reported nothing, losing `gh`. The label is now stepped over like a `name=`
  prefix.
- Only the first heredoc opener on a line was tracked, so `cat <<A <<B` left
  the second body and its terminator read as commands (`B`, `y`). Openers are
  queued and consumed in order.
- Masking an expression to a QUOTED token let the quote-stripper delete it
  entirely, so `${{ steps.x.outputs.cmd }} arg` reported `arg` as the command.
  The token now survives stripping and cannot match a command word, so an
  expression in command position contributes nothing.

Ten further adversarial shapes were already correct and are pinned as
regression guards: nested `$( )` in quotes, subshells, function definitions,
indented heredoc terminators, backticks, bare redirects, adjacent and empty
`${{ }}` sites, and a JSON literal inside an expression. Verified to flip —
exactly the three above fail against the previous commit, the other ten pass.

434 real steps re-swept: 0 non-verbatim bodies, 0 live header lines, 0 missed
expression sites, 0 out-of-order env, 0 `bash -n` failures.

* fix(review): annotate the continuation accumulator so tsc can type it

`invokedCommandsOf`'s backslash-continuation loop failed to compile with
TS7022: the narrowed type of `pending` at the join line is the union of
the loop-entry value and the back edge, and the back edge is computed
from the join itself. The declaration's own annotation does not break
that cycle - control-flow narrowing runs after it - so the checker gives
up and calls the result `any`.

Caught only on a forced rebuild. `tsc --build` had been reporting this
tree clean off a stale .tsbuildinfo, which is the same shape of gap that
put a type error into CI last week: the test runner strips types, so the
suite stayed green either way. Gate with --force.

* fix(review): a file-count label stops counting at the end of its line

`Test Files  45 passed` filing its 45 as a differing TEST count was
fixed once, with a lookbehind on the bare-count pattern. That only ever
rejected the all-green shape. The moment any file fails, the runner
prints `Test Files  1 failed | 44 passed (45)`, the label is no longer
adjacent to the number, and 44 comes through as a test count - so the
note reads "claimed 44, observed 1323" on exactly the runs whose summary
someone would paste. Adjacency was the wrong invariant; the line is.

Masking from the label to end-of-line is distance-independent and picks
up jest's `Test Suites: 1 failed, 44 passed, 45 total` at no cost. The
label keeps its `Test` word on purpose: a first cut matched a bare
`files` and blanked the line in "expect all four files and 471 tests to
pass", silencing a real claim. An existing test caught that, which is
the argument for the rule being as narrow as it is - anything that
suppresses claims is worth exactly its narrowness.

Also drops the now-dead lookbehind, which a reviewer had separately (and
wrongly) called a JS syntax error; variable-length lookbehind is legal
in V8, and the module parsed fine. It goes because the line rule
subsumes it, not because it was broken.

* fix(review): parse the base rerun before its output is trimmed

`trimOutput` rescues module errors and runner summaries out of the
omitted middle, not the per-file FAIL lines this command reads. A base
suite whose failure section overruns the tail budget therefore lost
failing files into the gap - and a SHORT base set is the dangerous
direction, because netNew is the PR side minus the base side. Every file
the trim hid came back as a Critical attributed to this PR by
"measurement". Parse the raw text, report the bounded one.

The PR side cannot be fixed here: it is read out of build-test's stored
output, trimmed before this command existed. That loss runs the other
way - it understates `shared`, never invents a netNew - so the entry
carries `prTruncated` and the note says the list may be partial. A
silence-biased gap is still a gap the author should hear about.

Also names both selection caps in the mutant-skip diagnostic. The count
accumulates replacement sub-cap drops, and with 2 deletions and 6
replacements the total is exactly MAX_MUTANTS: the main cap never fires,
yet 3 are dropped, and the message sent the reader looking for a pool of
11 candidates that never existed.

* test(review): pin the `sh` set-line to exactly what the runner uses

A reviewer asked for `expect(script).not.toContain('set -e')` on a
`shell: sh` step. The opposite is correct - GitHub runs that step as
`sh -e {0}` - but the thread was right that nothing pinned it either
way. Assert both halves: `set -e` is present, `pipefail` is not. Dropping
the line makes an extracted `sh` step run past a failure the runner
stops on; adding pipefail claims a bash feature `sh` does not have.

* fix(review): rerun only the command shapes build-test emits

This command reads a report off disk and then executes the strings in it
with `shell: true`, in the base worktree. Nothing else in the pipeline
re-executes a value it read back from a file, so nothing else has to
care where that value came from - this does, and the provenance is worse
than it looks: the command is `npm test --workspace="<dir>"`, the
workspace token is a directory, and a directory is a name a pull request
chooses. `packages/x";curl …|sh;"` is a legal path in git and on Linux
and it round-trips through the report into a shell.

Restricting to the emitter's own grammar costs nothing real, since that
grammar is the two shapes build-test produces. A command outside it is
skipped and disclosed, the same treatment everything else this command
cannot do already gets, so a future shape degrades to "judge it by the
diff" rather than to arbitrary execution.

* fix(review): the working directory is a setting the extraction was losing

Two findings, both of them this file failing its own stated argument.

The stub list dropped it. `expressions` covered the script and the env
and nothing else, so a `working-directory: ${{ github.workspace }}/x`
produced an empty list and a summary line reading "0 ${{ }} site(s) to
stub" - and `expressionsOf`'s own comment says why that is the failure
this list cannot afford: the caller reads it as "these are all the
values to supply". Widened to every setting the command carries, the
`shell:` template included.

The emitted script never mentioned it. The env block is commented into
the header precisely so a reader of the script alone can see it; the
working directory changes what the script does just as much and was in
the metadata only. The argument for reading all three levels, written in
this file, is that a step run "in the wrong directory, and nothing says
so" is the transcription error the command exists to remove - which is
exactly what the header did. It is a comment rather than a `cd` for the
same reason env is comments, not exports: the value may hold `${{ … }}`
and this command substitutes nothing.

Both pinned, and both checked by deleting the fix: each mutation fails
exactly one of the two new tests.

* fix(review): $(( )) is arithmetic, and a heredoc's form decides where it ends

Three fixes and one deliberate non-fix, all measured against this repo's
own 434 `run:` steps rather than argued.

`$(( ))` was read as a command substitution, so `N=$((N + 1))` reported
`N` as a command to stub. It was the single largest source of junk in
the list: 196 distinct "commands" across the corpus, 165 without it.

A plain `<<WORD` heredoc ends only on a line that is exactly WORD; the
loose match ended it on an indented `EOF` inside the body and then read
the body as commands, which is how `rm` got reported for a script that
never runs it. `<<-` stays looser than bash (any leading whitespace, not
just tabs) because looser can only end a body early, and this file's
priority is that an under-report is the worse direction.

`[^()]*` matched only the innermost `$( )`, so `X=$(gh api $(u))` lost
`gh` - a missed stub, and the extraction reaches the network. Depth
counted now, and the assignment-prefix skip no longer steps over an
unclosed `X=$(gh` into reading `api` as the command.

The non-fix is recorded where the next reader will hit it: the quote
walk is flat while shell quoting nests, and over ~300 lines the drift
reports fragments of jq source as commands. Inserting a separator where
a blanked span was removes nine of those, but it splits `a"X"b`, which
is one word to the shell, and the minimal reproducer for the difference
is 296 lines - nothing short enough to pin it. A scanner nobody can pin
costs more than the junk it removes.

* fix(review): refuse an ambiguous step name, and report errors like the siblings

Two findings, both about a message the caller never gets.

A job may legally hold two steps with the same name. The selector took
the first and said nothing, which is the failure this file's own header
names - "picks the same-named step from the wrong job" - and it is worst
in the use the command exists for: A/B extraction runs it once per tree,
so a PR that adds or reorders a duplicate leaves the two sides comparing
different steps while reporting on one. Refused out loud now, naming the
indices; the index is always available and never ambiguous.

The handler also let every throw propagate, so five carefully separated
messages - cannot read vs cannot parse vs no job vs no step vs no `run:`
- all arrived as "An unexpected critical error occurred" under a stack
trace. `base-tree` and `test-plan` in the same directory already catch,
write the message, and set exit 1. Matched. The separation between "the
path is wrong" and "the YAML is wrong" only pays if the caller sees it.

* docs(review): the brief's list of limits was one short

"Two limits worth knowing before you spend the step" became three when
the selector started refusing an ambiguous step name, and the count went
stale in the same commit that made it wrong. This is the enumeration
drift the skill teaches, in the text that teaches it.

The added entry says what to do rather than only what happens: pass the
index, which is what an A/B wants regardless — the two trees have to
select the same step, and a name that moved between them is exactly how
they stop doing that.

* chore(review): drop a scratch probe file that reached the branch

`packages/cli/inert.mjs` was a throwaway harness for sweeping the repo's
own workflows; its cleanup ran with a relative path from the wrong
directory and it got committed by the next `git add -A`.

It failed CI and not the local hook because the two lint different sets:
lint-staged filters to `*.{js,jsx,ts,tsx}`, which does not include
`.mjs`, while CI's flat config picks the file up regardless of `--ext`.
Verified with CI's own command this time, not the hook's.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
@wenshao
wenshao deleted the feat/review-borrowed-verification branch August 1, 2026 16:49
pchaganti pushed a commit to pchaganti/qx-qwen-code that referenced this pull request Aug 1, 2026
…M#8255)

* feat(review): borrowed-verification trio — test-plan check, base-tree A/B, per-hunk probes

- qwen review test-plan: rule on the PR Test Plan's checkable claims (paths,
  npm scripts, test counts) against the reviewed tree; contradictions and
  differing counts are disclosed via compose-review, never capping.
- qwen review base-tree: build the merge base in a sibling worktree so the
  verifier can A/B a comparative claim instead of reading it; swept by cleanup.
- test-efficacy: third probe kind — reverse-apply one hunk at a time and
  re-run the affected tests, attributing a still-green suite to the specific
  change nothing gates; shares the mutants' budget window, runs last.

* fix(review): survive real runner output — ANSI-laced and trimmed-away summaries

Both measured on a live /review of QwenLM#8176 with the built CLI:

- test-plan's observedTestCounts strips SGR sequences before matching; a
  color-enabled pipe interleaves them BETWEEN tokens, and the count claim
  fell to 'unchecked' with the summary right there in the report.
- build-test's trimOutput rescues runner summary lines from the omitted
  middle (like module-resolution errors): a failing suite's tail is all
  failure details and npm epilogue, which pushed the one-line summary out
  of the kept text entirely.

* fix(review): address the eight findings from live review of this branch

All measured in the review (QwenLM#8215 review comment):

- test-plan: linear-time bold-heading scan (the old pattern backtracked
  catastrophically on an unclosed ** line an untrusted PR body controls);
  a flag preceding the npm script yields no claim instead of a false
  'no package defines this script'.
- test-efficacy: a hunk probe's restore recreates the parent directory a
  reverse-applied 'new file' hunk removed (the ENOENT from finally lost
  the verdict and marked every remaining hunk inconclusive); hunks get
  their own skippedForBaseline instead of mislabeling a red baseline as
  a budget skip; splitDiffIntoHunks re-captures the file header at every
  diff --git boundary; a hunk-survived finding notes when it restates an
  inert file-level revert at hunk granularity.
- base-tree: idempotent fast path keyed on a build marker + HEAD check —
  concurrent verifier shards reuse one built tree instead of sweeping it
  out from under each other mid-A/B (a fabricated base-side difference
  with a deterministic source tag was the worst case); cost wording is
  now 'an install and a build' everywhere it was 'one extra build'.

* fix(cli): never score a hunk survived when its own test left the baseline (QwenLM#8215)

A per-hunk probe reported `survived` whenever the green baseline probes still
passed with the hunk reverted. When the hunk's own collocated test dropped out
of the baseline (a probe-tree import error collects nothing), the remaining
green probes prove only that THEY do not cover the hunk, so the verdict is now
`inconclusive` — the same dropped-test asymmetry the mutants already hold.

Also scope the hunk-survived cross-reference note to the hunk's own collocated
test, and let test-plan match a workspace-scoped run of the plan's bare command
instead of falling through to the manifest on an exact-string miss.

* fix(review): silence-bias hardening from four live review rounds of this branch

The two blocking findings, reproduced on this PR's own Test Plan:

- test-plan files no false contradicted notes: npm rulings move from a
  four-verb denylist to an allowlist (the run form + npm's script aliases —
  the ~fifty other builtins each used to become 'no package defines this
  script'); a slash token is claimed as a repo path only with evidence (an
  extension or ./ prefix), never when it is a flag's value (--repo
  owner/repo) or under the review's own temp root; HEADING_LINE_RE drops
  the same quadratic shape its bold sibling was rewritten to remove.
- base-tree gets a real mutual-exclusion lock around sweep+add+build
  (mkdirSync test-and-set; the loser returns busy instead of deleting the
  tree the winner is mid-install in), and a failed build writes a settled
  marker so later shards stop re-paying the install to relearn
  'unavailable'.

Also: Agent 7's brief now names hunk-survived and the hunks.* counters
(it is the report's only consumer, and the finding class was invisible);
hunk findings anchor at the first ADDED line instead of up to three
context lines above the change.

* feat(review): four round-2 borrowings — measured failure attribution, round ledger, richer mutants, doc parity

Shaped by a live round-2 maintainer re-verification (QwenLM#7818):

- qwen review test-delta: rerun the PR side's failed test commands on the
  built merge base and diff the failing FILE SETS — netNew is the PR's own
  failure by measurement (whatever files the diff touches), shared is
  pre-existing by measurement. Counts are never compared: a flaky suite
  fails different test names between runs of the same tree. An unfinished
  base rerun attributes nothing.
- Round ledger: the incremental review cache persists confirmed findings
  under round-scoped ids (R1-2); a high-effort re-review rules on each
  (fixed / still stands / cannot tell) and opens its report with the table,
  the way a human round-2 comment opens with 'M1 is fixed'.
- Three replacement mutation operators in test-efficacy: drop '?? fallback',
  force a comparison-bearing guard condition to 'true', drop a '+ CONST'
  term. Each survivor maps to one crisp untested-protection sentence.
  A line whose raw text and literal-blanked code view disagree yields no
  candidate — an edit index computed across the two views once spliced
  'iftrue 0)' into a guard, and a mangled mutant reads as inconclusive
  while quietly spending a cap slot.
- Quality brief: documentation-parity lens — a new user-facing surface whose
  siblings are documented is a Suggestion naming the sibling precedent; no
  documented sibling, no finding.

* feat(review): render adjudication, workflow step extraction, three verification lenses

Shaped by a live sanitizer-PR verification (QwenLM#8147):

- Render-adjudication capability (opt-in): with QWEN_REVIEW_SCRATCH_REPO
  set, the verifier may post a minimal payload to that user-designated repo
  and rule on GitHub's own rendered HTML — the measured case being an
  @ -> &QwenLM#64; defusal every local reading called sound while GitHub's real
  renderer registered the mention and fired the notification. Absent the
  setting, rendering claims honestly cap at low confidence / cannot tell.
  Step 7's write ban names the carve-out explicitly.
- qwen review extract-step: lift one workflow step's run: script verbatim
  into an executable, with env (as comments, never half-substituted
  exports), every ${{ }} site listed unevaluated, and a heuristic invoked-
  command list as the stubbing starting point. With base-tree, both arms of
  a by-hand workflow A/B become two invocations. yaml declared as a cli
  dependency (previously resolved only via hoisting).
- Three brief lenses: a borrowed protection idiom missing what made it work
  at home (the code ancestor did the protecting; only the entity was
  copied); a second parser for an authoritatively-parsed format is a
  divergence hunt; tests that pin the mechanism instead of the effect, and
  oracles that mirror the implementation's own model.

* feat(review): sibling-entrance discipline for the fixed verdict

From round 6 of the live sanitizer verification (QwenLM#8147):
the fix closed the fence-shaped entrance into a raw-HTML block, and the
code-span entrance beside it — same divergence, adjacent syntax — stayed
open. A re-check that tests only the reported input rules 'fixed' over a
hole one backtick away.

Both fixed-verdict sites in SKILL Step 6 (the open-Criticals re-check and
the round ledger) now require enumerating a divergence-class defect's
sibling entrances before ruling fixed; a still-open sibling is a NEW
finding, never a reason to withhold the original's fixed — the two
rulings stay separate so the second hole cannot ship unreviewed.

* feat(review): three measured-verification lenses from live rounds 8037/8005

- Threshold-boundary scan: when a fix is a ratio/length guard, hold the
  issue's own variables fixed and binary-search the boundary where the
  behaviour flips; put the number next to what the issue reports. Live
  case: a prose-ratio guard covered the edit/write_file half of its issue
  and silently declined the run_shell_command half (~473-char boundary).
- Delimiter self-injection named as the first parser-differential probe:
  a no-escaping extractor fed its own close tag truncates silently.
- Shared-gate state enumeration: a deliberate-design defence extends only
  to the states it argues — an input-hold argued for 'active' silently
  froze three idle states sharing the same gate. The sibling-entrance
  rule, applied to a state machine instead of a syntax.

* feat(review): carry the round ledger in the posted review body

The ledger shipped as a local cache file and its first multi-round live
use exposed the flaw: four model-comparison rounds re-reviewed the same
two PRs and every round opened from scratch — medium never reads the
cache, and from CI or another clone there is no cache at all. The one
artifact every environment can see, the posted review, carried nothing
machine-readable.

- compose-review embeds an HTML-comment marker (invisible on GitHub)
  carrying this round's findings and round number; the round comes from
  the side file pr-context recovered, +1 — never from the model.
- pr-context recovers the reviewing account's latest marker, renders it
  as a 'Previous /review round (machine ledger)' work-list section, and
  writes it beside the context file for compose-review's round counter.
  Own-account only, latest round wins, fail-quiet on any malformation —
  the marker is data the next round rules on, never authority it obeys.
- Step 6's previous-round ruling now runs at medium too (recovery is
  free; medium still writes no cache and posts nothing); the local cache
  is demoted to fallback for rounds that never posted.

* fix(review): address review feedback — false-positive hardening, binary diff guard, error convention (QwenLM#8215)

* fix(review): address review feedback — base-tree availability gate, test-plan false positives, hunk-probe ranges (QwenLM#8215)

- base-tree: only stamp a base tree available when runBuildTest actually
  compiled something (ok AND npm toolchain AND a non-empty build). An
  `unsupported` handoff or an empty npm scope returns ok:true having built
  nothing; marking that tree available let an A/B read the absence of a build
  as a behavioural difference.
- cleanup: sweep the stale base-tree build lock a killed builder leaves behind.
- test-plan: read the root manifest's scripts directly so a root-only script
  survives when the root defines no build/test; bail on the inline --root=./dir
  rebasing form; stop treating a positional after an inline --flag=value as the
  flag's value; prefer a failed scoped run when ruling a bare command; anchor
  the npm script alias to a full token so `yarn test:unit` is not truncated.
- test-efficacy: exclude `\ No newline at end of file` from the startLine offset
  count; compute the mutant-overlap range from the header's new-side span so it
  no longer overshoots into a closely following hunk.

* fix(review): address review feedback — diff-header false positives, stale prompt enumeration, added-file hunk probes (QwenLM#8215)

* fix(review): address review feedback — cd-base exclusion, Test Files count guard, base-tree error handling, probe delegation (QwenLM#8215)

* fix(review): port the collocated-dropout test to the post-QwenLM#8050 runner seam

Merging main brought QwenLM#8050's Windows-portability refactor, which resolves
the probe runner through vitest/package.json's bin — a node_modules/.bin
fake is dead weight it never reads. The 8215-only collocated-dropout test
still installed the old .bin fake, so the REAL vitest ran its fixtures,
price.test.ts genuinely passed, and the hunk scored survived. The test now
overrides the fake package's vitest.mjs like every post-refactor test.

* fix(review): bound the summary rescue, apply the ATX heading rule, sweep stale build locks

The three 8215-layer findings from the latest review, fixed at this
layer (they were first patched further up the stack, where the reviewer
of THIS PR cannot see them):

- trimOutput's summary rescue is capped at 40 lines — uncapped, 40k
  lines of 'Test <n>: …' prose voided the trim entirely (measured 1.6MB
  in, 1.6MB out) and the bounded-output contract is the whole point.
- A '#' with no following whitespace is prose, not a heading (the ATX
  rule GitHub applies): 'QwenLM#8176', '#tag', an unfenced '#!/bin/bash' no
  longer end the Test Plan section mid-body; the bare-#-run crash on
  the closing scan is guarded.
- A base-tree build lock older than 30 minutes is a corpse left by a
  killed builder — swept and rebuilt instead of reporting busy for the
  rest of the review.

* fix(review): EEXIST-only lock busy, bun test alias, chained cd bail, fence backreference

Four live findings from the latest inline review round (the rest of the
round was already fixed upstream by the takeover bot - verified by
probing head behavior rather than re-reading the threads):

- base-tree's lock catch distinguishes EEXIST (a concurrent builder,
  busy) from EPERM/EROFS/ENOSPC (this run's own failure, reported as
  such, not as a busy that will never clear).
- "bun test" is bun's built-in runner, not a package-script alias: it
  runs whether or not any manifest defines test, so ruling it against
  the scripts table filed a false contradicted.
- A chained cd matches the leading-cd shape but the single-hop resolver
  joined file tokens against the FIRST directory; it now bails like the
  exotic-cd case.
- codeSpans' fence regex closes on its own marker via backreference; a
  tilde fence line inside a backtick block ended the span early and
  lines after it were lost to extraction.

* fix(review): make the ledger marker actually reach the posted body

Three Criticals from the QwenLM#8255 review, all real:

- The marker was appended in the CLI handler, AFTER composeReview()
  returned, so it only ever reached the composed JSON on disk. submit
  posts what the PURE function returns, so no posted review carried a
  marker, latestOwnLedger always returned null, and the whole feature
  was inert end to end. The append now lives inside composeReview, fed
  by a draftedComments seam that both CLI boundaries fill from the same
  array they count (and both strip from a model-written state JSON).
  Three end-to-end tests now assert on the body composeReview returns.
- pr-context's side-file write preceded the mkdir that creates its
  directory and was unguarded: an ENOENT failed the whole command over
  a best-effort carry-forward. mkdir first, write guarded.
- CANONICAL_LGTM_RE is anchored, so a trailing marker made every no-op
  round "worth showing" and prior rounds started rendering in full. The
  marker is now stripped before the filter and the count walk, not only
  before rendering.

Plus the three thread findings: ledger cells are escaped before landing
in the context table (a title with a pipe could forge rows), parseLedger
takes the LAST marker and type-checks/normalises what it recovers, and
the stray verdict doc comment is reattached to verdictLine.

* fix(review): close the ten open findings on this PR

- guard-true tested for a comparison anywhere after `if (`, including the
  then-body, so `if (ready) emit(a !== b);` admitted a mutant on the
  comparison-less condition the gate exists to exclude. It now tests the
  condition span only.
- The `survived` detail said "when it changes" for legacy DELETION
  mutants too; it now matches the operator.
- test-delta's `unparsed` required both sides to parse zero files, so a
  PR-side failure whose FAIL lines the trim scattered was silently
  dropped whenever the base rerun happened to parse. netNew/shared come
  from the PR side, so the PR side alone decides.
- failingFilesOf now matches Windows path shapes (backslashes, C:) - a
  missed parse is an unattributed failure, not a loud error.
- The replacement branch of runOneMutant (write-file -> run-probe ->
  classify) had no end-to-end test; one now drives a coalesce operator
  through the real handler and asserts the mutated line, the verdict,
  the operator-specific wording, and that the shared tree is untouched.
- Two tests were vacuous with respect to what they promised: the
  baseline-dir test never asserted the cwd (its helper swallowed the
  argument - fixed at the helper), and the one-candidate-per-line test
  used an input that never triggered the replacement path, so the
  `continue` under test was not load-bearing.
- Reattached the orphaned selectMutants JSDoc; reworded the SKILL line.

* fix(review): the test helper's cwd parameter is required, not optional

CI's `tsc --build` failed on test-delta.test.ts: the exec seam always
passes a cwd, but the helper's signature marked it optional, so pushing
it into a string[] was `string | undefined`.

Missed locally because vitest runs through esbuild, which strips types
without checking them - the suite was green while the build was red. The
gate to run before pushing a type-level change is `npx tsc --build`, not
the test suite.

* fix(review): a base rerun that could not RUN attributes nothing

Two Criticals from the latest review, both reachable on the brief's own
happy path:

- baseUnusable covered only timeouts. Every other way the base side can
  fail to run - an unbuilt base tree, a missing install, a workspace the
  PR ADDED (npm test --workspace cannot resolve on base), an ENOBUFS
  truncation - exits non-zero with zero FAIL lines, which this code read
  as a green base. Every PR-side failure then became netNew: the
  strongest evidence the command emits, manufactured from a base that
  never ran a test. It now attributes nothing and says why.
- Timeout detection was the weaker substring form the sibling explicitly
  rejects; an external SIGTERM (container stop, cancelled job) set
  neither an ETIMEDOUT message nor an exit code and fed straight into
  the above. build-test now exports spawnTimedOut and test-delta asks
  the same question rather than re-deriving it.

Also: the base output is trimmed (it precedes the verdict fields in the
report the agent reads, so an untrimmed megabyte truncates exactly what
the command produces); the guard-true gate no longer reads an arrow
function's => as a comparison (every predicate guard was a candidate -
the if (ready) noise the gate exists to exclude); the term-drop message
no longer calls a string concatenation a reserve term; the unparsed note
describes its own PR-side-only condition; and the ledger's Step 6/Step 8
now agree that a still-standing finding keeps its id.

* fix(review): extract-step resolves all three env/defaults levels, and comments every env line

Two silent-wrongness defects in a command whose whole value is fidelity.

`env:`, `shell:` and `working-directory:` are three-level settings on GitHub —
workflow, job, step, nearest wins — and only the step level appears in the
step's own text. Reading step-level alone reproduced by machine the exact
transcription error this command exists to remove: measured, a step under a
job-level `NODE_ENV: production` and a workflow-level `GLOBAL_FLAG` extracted
with `env: { LOCAL: '1' }` and `workingDirectory: undefined`, so the emitted
script ran with both unset and nothing said so. Not a contrived shape: this
repo carries workflow-level `env:` in 7 workflows, job-level `env:` in 10, and
job-level `defaults.run` in qwen-triage.yml — the workflow the command's own
test plan names. The three levels now merge with the runner's precedence, and
`envSources` records which level each key came from, so an inherited value is
visible rather than indistinguishable from the step's own.

The env block was commented per ENTRY, not per LINE. A YAML block scalar
(qwen-autofix.yml's `SETTINGS_JSON: |-`) reaches the header as a multi-line
string, so its continuation lines landed in command position — and under the
`set -e` the header itself emits, the extracted step died in its own preamble
before its `run:` body ran. Every line is commented now.

Tests pin the effect, not the mechanism: `executableLines()` asserts nothing
but the `run:` body ever reaches command position, plus a `bash -n` parse
check. Verified to flip — all five new assertions fail against the pre-fix
implementation (`{ LOCAL: '1' }`, `undefined` working directory, three
executable lines instead of one, and a real `bash -n` syntax error).

* fix(review): compare failing files by a normalised, project-keyed identity

Critical: the two sides run in DIFFERENT roots (the PR worktree and the
base tree), and netNew/shared compared the parsed paths verbatim - so an
absolute-path runner turned every pre-existing failure into a fabricated
Critical, with the authority of a measurement behind it. Paths are now
normalised against each run's own root (and backslashes to /, so a
Windows path compares with its POSIX-printed twin), which is why
test-delta gained --pr-worktree.

The identity also keeps the vitest project token: dropping it collapsed
same-named test files across workspaces, so a PR-caused failure in one
package could read as pre-existing because another package has a file by
the same name - the worse failure direction.

Also from the same review, all of them reachable on the brief's own path:

- The base rerun now inherits build-test's stdio: ['ignore','pipe','pipe']
  ("a build that asks a question is a build that hangs until the
  deadline") and its trimOutput, which matters because entries[].base
  precedes the verdict fields in the report the agent reads.
- The brief gates on base-tree's `available`, not just its `path`: a tree
  that was created but did not build populates path too, and measuring
  against it turns an infrastructure failure into Criticals.
- A programmatic caller omitting `timeout` no longer sends NaN into
  spawnSync.
- MutantCandidate is a discriminated union, so an operator without its
  replacement line - which would delete a line while reporting "with its
  ?? fallback dropped" - is unrepresentable.
- The comparison class no longer requires a trailing space (if (a<b) is
  the same guard, just unformatted) and matches a brace-less else if.
- DeltaEntry.unparsed's doc now describes the PR-side-only condition it
  actually implements.

* fix(review): restore the whole-command budget, keep generics out of guard-true

Round-1 findings from a fresh review of this PR:

- test-delta had no aggregate deadline: --timeout is PER command and
  defaults to 300s, so three failed commands is 900s against Agent 7's
  600s ceiling - killed with NO report at all, discarding the base-tree
  install and build just paid for. TOTAL_BUDGET_MS mirrors the one
  test-efficacy reserves; commands it cannot fit are disclosed.
- guard-true matched generic calls: `if (isRecord<string>(v))` produced a
  mutant, and a type-guard predicate is exactly the `if (ready)` shape
  whose survivors the gate calls noise. The trailing \s is required, not
  an accidental asymmetry with [!=]== - telling `a<b` from `fn<T>(x)`
  needs a parser, and the gate is silence-biased by design.
- --pr-worktree had no contract test, and its failure mode is the worst
  here: arriving undefined, root stripping silently stops and every
  pre-existing failure becomes a fabricated netNew. The new test feeds
  parseSync's output straight into runTestDelta and asserts an
  attribution only reachable when both roots were stripped (verified
  red against the snake_case field shape that shipped once already).
- Merged the two consecutive doc comments on prWorktree.

* fix(review): a budget-shortened deadline is not the same fact as a slow rerun

Round-2 finding on the budget just restored: `Math.min(perCommandMs,
remaining)` can hand a rerun far less than --timeout, and if it dies
there the note said only "timed out - infrastructure, not evidence".
True, but it sends the reader hunting a hang that is really an exhausted
budget - and unlike a real timeout, a rerun with budget to spare would
still measure it. The note now names those commands separately and says
so. Verified red against removing the tracking line.

* fix(review): brace-tolerant stub list, pipefail fidelity, and extract-step in the briefs

Round-3 findings on this PR, fixed.

`expressionsOf` matched `[^}]*`, so any expression containing a brace —
`format('refs/pull/{0}/head', …)`, `fromJSON('{"a":1}')` — was not mis-listed
but DROPPED. A stub list reads as "these are all the values to supply", so a
silent omission is a value that never gets stubbed. It now scans forward to the
closing `}}`, and reports nothing for an unterminated site rather than
swallowing the rest of the text.

Declaring `shell: bash` is not the runner's default `bash`. The default is
`bash -e {0}`; a declared `bash` (at any level) is
`bash --noprofile --norc -eo pipefail {0}`, and a pipeline whose middle stage
fails aborts under one and not the other. The header now carries `set -eo
pipefail` or `set -e` accordingly — 163 of this repo's 434 `run:` steps are
under a declared bash and were getting the weaker one. A `shell:` value is also
a command template (`perl {0}`), so only its first word goes in the shebang and
the whole template is recorded beside it.

`extract-step` was registered on the CLI and mentioned in DESIGN.md, and
nowhere in SKILL.md or the agent briefs — the runtime prompts. The capability
was unreachable by the agents it was written for. The verifier's brief now
carries it next to the A/B paragraph it composes with, and Step 4 summarises it.

Also: env ordered nearest-first (measured on qwen-autofix.yml:route:0, merge
order put 20 inherited entries ahead of the step's own 26 in a 49-line header);
a valueless `FOO:` renders as the empty string, not `"null"`, and a non-scalar
as JSON rather than `[object Object]`; a missing file no longer reports as a
parse failure; DESIGN.md's lens count matches its list.

The test oracle is rebuilt around the property instead of a filter: the file is
the header plus the body verbatim, and every line before the body is a comment
or a directive the test names. The old helper dropped `set -e` unconditionally,
so it could not tell the header's from one the body legitimately contains — and
would have gone green on a header that leaked exactly that line. 434 real `run:`
steps swept: 0 non-verbatim bodies, 0 live header lines, 0 missed expression
sites, 0 out-of-order env, 0 `bash -n` failures.

* fix(review): restore the replacement sub-cap, stranded on a downstream branch

Round-3 finding, and the third instance of one class: an 8218-layer fix
committed on the 8261 branch, four PRs above the code it belongs to.

Measured over 40 real commits, the replacement operators produce ~24x
the deletion pool (215 vs 9 candidates; guard-true drives it). Every
mutant run drains the same window hunk probes draw from LAST, so
uncapped, most diffs with any replacement candidates leave hunk probing
zero runs - the hunk-survived finding class silently stops firing and
nothing says so. Three slots, and what the sub-cap drops is counted in
skippedForCap rather than lost.

Also swept the other direction: diffed every review file against its
8261 copy to confirm nothing else 8218-layer is stranded up there. The
remaining divergence is 8261's own (the positive control, its lenses).

* fix(review): one severity predicate, carried ids, and a lossless marker

Round-1 review of the ledger marker, six findings.

Two contradicted contracts this package already states elsewhere:

- `buildLedger` restated the severity predicate as a bare `startsWith`
  while `severityOf` — what `countInlineFindings` and the unmarked-comment
  gate both use — trims first. A Critical whose body opened with a newline
  was counted, was posted, blocked the merge, and was silently absent from
  the ledger, shifting the id of every finding after it. It classifies
  through `severityOf` now, and strips the marker with the shared prefixes
  instead of a second copy of the literals.

- Ids were assigned by array position every round, so the report saying
  `R1-2 still stands` shipped beside a marker recording that claim as
  `R3-4` — and `LedgerFinding.id` documented itself "stable across
  re-reports". Step 6 now mandates writing the original id into the
  re-report (`**[Critical]** R1-2: …`), `buildLedger` reads it back, and a
  duplicate or absent id falls back to the next free id of this round.

Four smaller ones:

- `stripLedgerMarker` removed only the FIRST marker while `parseLedger`
  reads the LAST, so a body holding two leaked the trusted marker into
  model-facing prose and un-filtered the canonical LGTM it was appended to.
- The `--` guard rewrote data to an em dash: a finding about `--comment`
  reached the next round as `—comment`. It escapes at the JSON layer now,
  which is comment-safe and lossless.
- `file` was capped on read (200) but not on write; the cap is one
  constant binding both halves.
- `submit`'s path — the one GitHub's payload is built from — had no test
  that the marker reaches the posted body.

Plus: tie-break `latestOwnLedger` on review id when `submitted_at` ties,
neutralise a backtick in the location code span, skip the `currentUser()`
round-trip when the PR has no reviews, and say why a stale side file is
deliberately left in place.

* docs(review): complete the "delta cannot rule" enumeration in both places

Round-4 finding. The brief and SKILL.md each listed three cases where
test-delta attributes nothing - unparsed, timed-out base, no merge base -
but the code has five: the later rounds of this PR added "a base rerun
that failed without naming any failing file" (it did not measure the
base) and "a command the whole-command budget could not fit". Two
enumerations of the same set with different membership, in the two
places an agent reads.

That is the sibling-enumeration lesson this skill teaches, applied to
its own prose for the second time: the fix is not just adding the
missing members but saying that the report names each case with its own
reason rather than folding them into one.

* fix(review): the invokes list was mostly prose, not commands

Round-4 finding on this PR. `invokes` is documented as a heuristic starting
point, and imprecision is fine — but measured over this repo's 434 real `run:`
steps it was reporting 435 distinct "commands", 267 of them appearing exactly
once, with a worst case of 63 entries made up of words like `CI`, `Evidence`,
`PR` and `and`. A list that size, mostly prose, is not a starting point.

Three causes, each measured:

- A `${{ … }}` expression is not shell, and it routinely contains `||`.
  Splitting on that as a pipeline separator reported both operands as commands
  (`matrix.arch`, `github.event.inputs.version`). Expressions are now masked to
  an opaque token before the split; one sitting in command position contributes
  nothing, which is honest — what it expands to is unknown here by design.
- A heredoc body is input to a command, not a list of them. Its lines were
  scanned as commands, terminator included. 12 steps in this repo carry one.
- The `name=value` skip stepped over the prefix and took the NEXT word as the
  command — but for a quoted value with spaces that word is inside the value:
  `EVIDENCE_SECTION=$'### Evidence images'` reported `Evidence`. Quoted spans
  are now blanked out, with the quote carried across lines so a multi-line
  string's continuation lines are data too. Command substitutions are read
  first, so `body="$(sanitize < "$REPORT")"` still reports `sanitize`.

A `#` preceded by whitespace ends the live part of a line, so an apostrophe in
a trailing comment cannot open a span and eat the rest of the script.

Measured after: 435 distinct commands to 187, singletons 267 to 104, worst case
63 to 27 — and the worst case is now real commands (`awk cat chmod curl git jq
mktemp pkill tar tee timeout`) plus the script's own shell functions. Also
added the builtins a stub could not intercept anyway (`eval`, `exec`, `source`,
`unset`, `command`, …) to the keyword set.

434 steps re-swept: 0 non-verbatim bodies, 0 live header lines, 0 missed
expression sites, 0 out-of-order env, 0 `bash -n` failures.

* docs(review): the rationale named only one of the ways base goes unmeasured

Third and last copy of the enumeration the previous commit fixed. The
bullet's headline already generalised - "base attributes nothing it did
not finish" - but its body named only the timeout, so a reader learning
the contract from the rationale would conclude timeouts are the only
unusable case. Name the set, and say why the report keeps the reasons
apart: "we could not measure" and "we measured nothing" are different
facts to the author, and only one of them is about their PR.

* fix(review): a quoted `<<EOF` is not a heredoc, and a continued line is one command

Round-5 findings, both in the previous commit's own scanner.

A heredoc opener was matched over the whole line, so one inside a string
started heredoc mode: `echo "write <<EOF for a heredoc"` made every later line
wait for a terminator that never arrives. The failure is not a missing entry
but a missing REST — measured, a three-line script returned `[]` instead of
`[curl, jq]`, empty and entirely plausible. Opener detection moved inside the
quote walk, where it only fires outside quotes; the quoted forms (`<<'EOF'`)
are consumed by the match, so their quotes never open a span either.

A backslash-continued command was scanned as several lines, which puts the next
ARGUMENT in command position — this is where `apt-get install -y \` /
`  libx11-dev` reported the package as an invoked command. Continuations are
now joined into one logical line before scanning.

Measured after: 187 distinct commands to 185, and the singleton tail is now
dominated by real ones — PowerShell cmdlets, macOS tooling, and the scripts'
own shell functions. 434 real steps re-swept: 0 non-verbatim bodies, 0 live
header lines, 0 missed expression sites, 0 out-of-order env, 0 `bash -n`
failures.

* fix(review): case labels, a second heredoc, and an expression in command position

Round-6 findings, from running the scanner against adversarial shapes rather
than reading it again. Two are UNDER-reports, which is the worse direction: a
command missing from the list is a stub the verifier never writes, so the
extraction reaches the real network.

- A `case` pattern label stopped the scan on its own line: `blocked) gh api x`
  reported nothing, losing `gh`. The label is now stepped over like a `name=`
  prefix.
- Only the first heredoc opener on a line was tracked, so `cat <<A <<B` left
  the second body and its terminator read as commands (`B`, `y`). Openers are
  queued and consumed in order.
- Masking an expression to a QUOTED token let the quote-stripper delete it
  entirely, so `${{ steps.x.outputs.cmd }} arg` reported `arg` as the command.
  The token now survives stripping and cannot match a command word, so an
  expression in command position contributes nothing.

Ten further adversarial shapes were already correct and are pinned as
regression guards: nested `$( )` in quotes, subshells, function definitions,
indented heredoc terminators, backticks, bare redirects, adjacent and empty
`${{ }}` sites, and a JSON literal inside an expression. Verified to flip —
exactly the three above fail against the previous commit, the other ten pass.

434 real steps re-swept: 0 non-verbatim bodies, 0 live header lines, 0 missed
expression sites, 0 out-of-order env, 0 `bash -n` failures.

* fix(review): annotate the continuation accumulator so tsc can type it

`invokedCommandsOf`'s backslash-continuation loop failed to compile with
TS7022: the narrowed type of `pending` at the join line is the union of
the loop-entry value and the back edge, and the back edge is computed
from the join itself. The declaration's own annotation does not break
that cycle - control-flow narrowing runs after it - so the checker gives
up and calls the result `any`.

Caught only on a forced rebuild. `tsc --build` had been reporting this
tree clean off a stale .tsbuildinfo, which is the same shape of gap that
put a type error into CI last week: the test runner strips types, so the
suite stayed green either way. Gate with --force.

* fix(review): a file-count label stops counting at the end of its line

`Test Files  45 passed` filing its 45 as a differing TEST count was
fixed once, with a lookbehind on the bare-count pattern. That only ever
rejected the all-green shape. The moment any file fails, the runner
prints `Test Files  1 failed | 44 passed (45)`, the label is no longer
adjacent to the number, and 44 comes through as a test count - so the
note reads "claimed 44, observed 1323" on exactly the runs whose summary
someone would paste. Adjacency was the wrong invariant; the line is.

Masking from the label to end-of-line is distance-independent and picks
up jest's `Test Suites: 1 failed, 44 passed, 45 total` at no cost. The
label keeps its `Test` word on purpose: a first cut matched a bare
`files` and blanked the line in "expect all four files and 471 tests to
pass", silencing a real claim. An existing test caught that, which is
the argument for the rule being as narrow as it is - anything that
suppresses claims is worth exactly its narrowness.

Also drops the now-dead lookbehind, which a reviewer had separately (and
wrongly) called a JS syntax error; variable-length lookbehind is legal
in V8, and the module parsed fine. It goes because the line rule
subsumes it, not because it was broken.

* fix(review): parse the base rerun before its output is trimmed

`trimOutput` rescues module errors and runner summaries out of the
omitted middle, not the per-file FAIL lines this command reads. A base
suite whose failure section overruns the tail budget therefore lost
failing files into the gap - and a SHORT base set is the dangerous
direction, because netNew is the PR side minus the base side. Every file
the trim hid came back as a Critical attributed to this PR by
"measurement". Parse the raw text, report the bounded one.

The PR side cannot be fixed here: it is read out of build-test's stored
output, trimmed before this command existed. That loss runs the other
way - it understates `shared`, never invents a netNew - so the entry
carries `prTruncated` and the note says the list may be partial. A
silence-biased gap is still a gap the author should hear about.

Also names both selection caps in the mutant-skip diagnostic. The count
accumulates replacement sub-cap drops, and with 2 deletions and 6
replacements the total is exactly MAX_MUTANTS: the main cap never fires,
yet 3 are dropped, and the message sent the reader looking for a pool of
11 candidates that never existed.

* test(review): pin the `sh` set-line to exactly what the runner uses

A reviewer asked for `expect(script).not.toContain('set -e')` on a
`shell: sh` step. The opposite is correct - GitHub runs that step as
`sh -e {0}` - but the thread was right that nothing pinned it either
way. Assert both halves: `set -e` is present, `pipefail` is not. Dropping
the line makes an extracted `sh` step run past a failure the runner
stops on; adding pipefail claims a bash feature `sh` does not have.

* fix(review): rerun only the command shapes build-test emits

This command reads a report off disk and then executes the strings in it
with `shell: true`, in the base worktree. Nothing else in the pipeline
re-executes a value it read back from a file, so nothing else has to
care where that value came from - this does, and the provenance is worse
than it looks: the command is `npm test --workspace="<dir>"`, the
workspace token is a directory, and a directory is a name a pull request
chooses. `packages/x";curl …|sh;"` is a legal path in git and on Linux
and it round-trips through the report into a shell.

Restricting to the emitter's own grammar costs nothing real, since that
grammar is the two shapes build-test produces. A command outside it is
skipped and disclosed, the same treatment everything else this command
cannot do already gets, so a future shape degrades to "judge it by the
diff" rather than to arbitrary execution.

* fix(review): the working directory is a setting the extraction was losing

Two findings, both of them this file failing its own stated argument.

The stub list dropped it. `expressions` covered the script and the env
and nothing else, so a `working-directory: ${{ github.workspace }}/x`
produced an empty list and a summary line reading "0 ${{ }} site(s) to
stub" - and `expressionsOf`'s own comment says why that is the failure
this list cannot afford: the caller reads it as "these are all the
values to supply". Widened to every setting the command carries, the
`shell:` template included.

The emitted script never mentioned it. The env block is commented into
the header precisely so a reader of the script alone can see it; the
working directory changes what the script does just as much and was in
the metadata only. The argument for reading all three levels, written in
this file, is that a step run "in the wrong directory, and nothing says
so" is the transcription error the command exists to remove - which is
exactly what the header did. It is a comment rather than a `cd` for the
same reason env is comments, not exports: the value may hold `${{ … }}`
and this command substitutes nothing.

Both pinned, and both checked by deleting the fix: each mutation fails
exactly one of the two new tests.

* fix(review): $(( )) is arithmetic, and a heredoc's form decides where it ends

Three fixes and one deliberate non-fix, all measured against this repo's
own 434 `run:` steps rather than argued.

`$(( ))` was read as a command substitution, so `N=$((N + 1))` reported
`N` as a command to stub. It was the single largest source of junk in
the list: 196 distinct "commands" across the corpus, 165 without it.

A plain `<<WORD` heredoc ends only on a line that is exactly WORD; the
loose match ended it on an indented `EOF` inside the body and then read
the body as commands, which is how `rm` got reported for a script that
never runs it. `<<-` stays looser than bash (any leading whitespace, not
just tabs) because looser can only end a body early, and this file's
priority is that an under-report is the worse direction.

`[^()]*` matched only the innermost `$( )`, so `X=$(gh api $(u))` lost
`gh` - a missed stub, and the extraction reaches the network. Depth
counted now, and the assignment-prefix skip no longer steps over an
unclosed `X=$(gh` into reading `api` as the command.

The non-fix is recorded where the next reader will hit it: the quote
walk is flat while shell quoting nests, and over ~300 lines the drift
reports fragments of jq source as commands. Inserting a separator where
a blanked span was removes nine of those, but it splits `a"X"b`, which
is one word to the shell, and the minimal reproducer for the difference
is 296 lines - nothing short enough to pin it. A scanner nobody can pin
costs more than the junk it removes.

* fix(review): refuse an ambiguous step name, and report errors like the siblings

Two findings, both about a message the caller never gets.

A job may legally hold two steps with the same name. The selector took
the first and said nothing, which is the failure this file's own header
names - "picks the same-named step from the wrong job" - and it is worst
in the use the command exists for: A/B extraction runs it once per tree,
so a PR that adds or reorders a duplicate leaves the two sides comparing
different steps while reporting on one. Refused out loud now, naming the
indices; the index is always available and never ambiguous.

The handler also let every throw propagate, so five carefully separated
messages - cannot read vs cannot parse vs no job vs no step vs no `run:`
- all arrived as "An unexpected critical error occurred" under a stack
trace. `base-tree` and `test-plan` in the same directory already catch,
write the message, and set exit 1. Matched. The separation between "the
path is wrong" and "the YAML is wrong" only pays if the caller sees it.

* docs(review): the brief's list of limits was one short

"Two limits worth knowing before you spend the step" became three when
the selector started refusing an ambiguous step name, and the count went
stale in the same commit that made it wrong. This is the enumeration
drift the skill teaches, in the text that teaches it.

The added entry says what to do rather than only what happens: pass the
index, which is what an A/B wants regardless — the two trees have to
select the same step, and a name that moved between them is exactly how
they stop doing that.

* chore(review): drop a scratch probe file that reached the branch

`packages/cli/inert.mjs` was a throwaway harness for sweeping the repo's
own workflows; its cleanup ran with a relative path from the wrong
directory and it got committed by the next `git add -A`.

It failed CI and not the local hook because the two lint different sets:
lint-staged filters to `*.{js,jsx,ts,tsx}`, which does not include
`.mjs`, while CI's flat config picks the file up regardless of `--ext`.
Verified with CI's own command this time, not the hook's.

* fix(review): bound the whole ledger marker, not just each of its fields

The per-field caps leave the total unbounded: fifty findings at full
width serialize to just under 17,000 characters. The module's own
paragraph calls the marker "a footnote, never a payload" and reasons
about GitHub's 65,536-character body limit, so the total is the number
that claim is about and nothing was computing it.

Set the budget from measurement rather than from the limit. Across every
review this pipeline has posted on its own stack (n=66) the body runs a
median of 721 characters, p90 2,178, max 3,925 - the limit has ~61 KiB
of headroom, and an over-long marker was never going to fail a post. The
real cost is proportion: 17,000 characters of invisible payload on a
3,925-character review is four times more marker than review. 8 KiB
holds fifty findings at realistic widths without dropping any, and the
truncation path is reached only by a ledger no round has produced.

What it drops travels with it. A `dropped` count is the difference
between a list the next round reads as complete and one that says it is
not - the same distinction this module already draws everywhere else.

* fix(review): a partial ledger must not render as a complete one

The size cap added in the previous commit can drop entries, and the
rendered section still opened with "**Every entry below is owed a
this-round ruling**" and said nothing about what was missing. The
`dropped` count existed and reached the only place a reader sees the
list as nothing at all — which is the failure it was added to prevent,
committed one step after adding the field. It now says the list is
partial, how many are missing, and that absence is not evidence a
finding was fixed.

Second, the table escaping escaped `|` but not `\`. `\|` in a title
became `\\|`, which markdown reads as an escaped backslash followed by a
LIVE separator: the forged row the escaping exists to prevent, produced
by the escaping. Backslash first now, and the test counts separators the
way markdown does rather than by pattern, because a lookbehind counter
is fooled by exactly the input under test.

* fix(review): an empty ledger title is a cap the next round cannot lift

A comment that is nothing but its severity marker produced a ledger
entry with an empty title. That does not merely degrade the entry, it
jams the review: the next round is told every ledger entry is owed a
this-round ruling, has no claim text to rule on, answers `cannot tell`,
and `cannot-tell-existing-critical` is one of the cap reasons. Nothing
between rounds changes, so the cap comes back every round.

Dropping the entry would hide a Critical that really was posted, so it
keeps its place and gets the one handle there is - the location it was
filed at, or "the review body" for a body-level one. The field's stated
job is "enough for the next round to re-locate the claim", and that is
what a text-less finding still has.

Found by probing `buildLedger` with ten malformed comment shapes; the
other nine - carried ids, id collisions, two comments carrying the same
id, `R2D2` not being an id, a leading newline before the marker - all
behaved correctly and needed no change.

* fix(review): count both caps as dropped, not just the byte one

Found by running the three previous fixes together on one round: 51
findings in, 24 kept, and the marker said 26 were missing.
`LEDGER_MAX_FINDINGS` truncates before the byte cap ever runs, and
measuring `dropped` against the already-sliced list left the count cap's
share silent - so the field added two commits ago to stop a truncated
list reading as complete was itself under-reporting the truncation.

Measured against what came IN now, and pinned across all four
combinations: count cap alone, byte cap alone, both, and neither. Kept
plus dropped equals given, in every one.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
pchaganti pushed a commit to pchaganti/qx-qwen-code that referenced this pull request Aug 1, 2026
…rol, seven lenses (QwenLM#8261)

* feat(review): borrowed-verification trio — test-plan check, base-tree A/B, per-hunk probes

- qwen review test-plan: rule on the PR Test Plan's checkable claims (paths,
  npm scripts, test counts) against the reviewed tree; contradictions and
  differing counts are disclosed via compose-review, never capping.
- qwen review base-tree: build the merge base in a sibling worktree so the
  verifier can A/B a comparative claim instead of reading it; swept by cleanup.
- test-efficacy: third probe kind — reverse-apply one hunk at a time and
  re-run the affected tests, attributing a still-green suite to the specific
  change nothing gates; shares the mutants' budget window, runs last.

* fix(review): survive real runner output — ANSI-laced and trimmed-away summaries

Both measured on a live /review of QwenLM/qwen-code#8176 with the built CLI:

- test-plan's observedTestCounts strips SGR sequences before matching; a
  color-enabled pipe interleaves them BETWEEN tokens, and the count claim
  fell to 'unchecked' with the summary right there in the report.
- build-test's trimOutput rescues runner summary lines from the omitted
  middle (like module-resolution errors): a failing suite's tail is all
  failure details and npm epilogue, which pushed the one-line summary out
  of the kept text entirely.

* fix(review): address the eight findings from live review of this branch

All measured in the review (QwenLM/qwen-code#8215 review comment):

- test-plan: linear-time bold-heading scan (the old pattern backtracked
  catastrophically on an unclosed ** line an untrusted PR body controls);
  a flag preceding the npm script yields no claim instead of a false
  'no package defines this script'.
- test-efficacy: a hunk probe's restore recreates the parent directory a
  reverse-applied 'new file' hunk removed (the ENOENT from finally lost
  the verdict and marked every remaining hunk inconclusive); hunks get
  their own skippedForBaseline instead of mislabeling a red baseline as
  a budget skip; splitDiffIntoHunks re-captures the file header at every
  diff --git boundary; a hunk-survived finding notes when it restates an
  inert file-level revert at hunk granularity.
- base-tree: idempotent fast path keyed on a build marker + HEAD check —
  concurrent verifier shards reuse one built tree instead of sweeping it
  out from under each other mid-A/B (a fabricated base-side difference
  with a deterministic source tag was the worst case); cost wording is
  now 'an install and a build' everywhere it was 'one extra build'.

* fix(cli): never score a hunk survived when its own test left the baseline (#8215)

A per-hunk probe reported `survived` whenever the green baseline probes still
passed with the hunk reverted. When the hunk's own collocated test dropped out
of the baseline (a probe-tree import error collects nothing), the remaining
green probes prove only that THEY do not cover the hunk, so the verdict is now
`inconclusive` — the same dropped-test asymmetry the mutants already hold.

Also scope the hunk-survived cross-reference note to the hunk's own collocated
test, and let test-plan match a workspace-scoped run of the plan's bare command
instead of falling through to the manifest on an exact-string miss.

* fix(review): silence-bias hardening from four live review rounds of this branch

The two blocking findings, reproduced on this PR's own Test Plan:

- test-plan files no false contradicted notes: npm rulings move from a
  four-verb denylist to an allowlist (the run form + npm's script aliases —
  the ~fifty other builtins each used to become 'no package defines this
  script'); a slash token is claimed as a repo path only with evidence (an
  extension or ./ prefix), never when it is a flag's value (--repo
  owner/repo) or under the review's own temp root; HEADING_LINE_RE drops
  the same quadratic shape its bold sibling was rewritten to remove.
- base-tree gets a real mutual-exclusion lock around sweep+add+build
  (mkdirSync test-and-set; the loser returns busy instead of deleting the
  tree the winner is mid-install in), and a failed build writes a settled
  marker so later shards stop re-paying the install to relearn
  'unavailable'.

Also: Agent 7's brief now names hunk-survived and the hunks.* counters
(it is the report's only consumer, and the finding class was invisible);
hunk findings anchor at the first ADDED line instead of up to three
context lines above the change.

* feat(review): four round-2 borrowings — measured failure attribution, round ledger, richer mutants, doc parity

Shaped by a live round-2 maintainer re-verification (QwenLM/qwen-code#7818):

- qwen review test-delta: rerun the PR side's failed test commands on the
  built merge base and diff the failing FILE SETS — netNew is the PR's own
  failure by measurement (whatever files the diff touches), shared is
  pre-existing by measurement. Counts are never compared: a flaky suite
  fails different test names between runs of the same tree. An unfinished
  base rerun attributes nothing.
- Round ledger: the incremental review cache persists confirmed findings
  under round-scoped ids (R1-2); a high-effort re-review rules on each
  (fixed / still stands / cannot tell) and opens its report with the table,
  the way a human round-2 comment opens with 'M1 is fixed'.
- Three replacement mutation operators in test-efficacy: drop '?? fallback',
  force a comparison-bearing guard condition to 'true', drop a '+ CONST'
  term. Each survivor maps to one crisp untested-protection sentence.
  A line whose raw text and literal-blanked code view disagree yields no
  candidate — an edit index computed across the two views once spliced
  'iftrue 0)' into a guard, and a mangled mutant reads as inconclusive
  while quietly spending a cap slot.
- Quality brief: documentation-parity lens — a new user-facing surface whose
  siblings are documented is a Suggestion naming the sibling precedent; no
  documented sibling, no finding.

* feat(review): render adjudication, workflow step extraction, three verification lenses

Shaped by a live sanitizer-PR verification (QwenLM/qwen-code#8147):

- Render-adjudication capability (opt-in): with QWEN_REVIEW_SCRATCH_REPO
  set, the verifier may post a minimal payload to that user-designated repo
  and rule on GitHub's own rendered HTML — the measured case being an
  @ -> &#64; defusal every local reading called sound while GitHub's real
  renderer registered the mention and fired the notification. Absent the
  setting, rendering claims honestly cap at low confidence / cannot tell.
  Step 7's write ban names the carve-out explicitly.
- qwen review extract-step: lift one workflow step's run: script verbatim
  into an executable, with env (as comments, never half-substituted
  exports), every ${{ }} site listed unevaluated, and a heuristic invoked-
  command list as the stubbing starting point. With base-tree, both arms of
  a by-hand workflow A/B become two invocations. yaml declared as a cli
  dependency (previously resolved only via hoisting).
- Three brief lenses: a borrowed protection idiom missing what made it work
  at home (the code ancestor did the protecting; only the entity was
  copied); a second parser for an authoritatively-parsed format is a
  divergence hunt; tests that pin the mechanism instead of the effect, and
  oracles that mirror the implementation's own model.

* feat(review): sibling-entrance discipline for the fixed verdict

From round 6 of the live sanitizer verification (QwenLM/qwen-code#8147):
the fix closed the fence-shaped entrance into a raw-HTML block, and the
code-span entrance beside it — same divergence, adjacent syntax — stayed
open. A re-check that tests only the reported input rules 'fixed' over a
hole one backtick away.

Both fixed-verdict sites in SKILL Step 6 (the open-Criticals re-check and
the round ledger) now require enumerating a divergence-class defect's
sibling entrances before ruling fixed; a still-open sibling is a NEW
finding, never a reason to withhold the original's fixed — the two
rulings stay separate so the second hole cannot ship unreviewed.

* feat(review): three measured-verification lenses from live rounds 8037/8005

- Threshold-boundary scan: when a fix is a ratio/length guard, hold the
  issue's own variables fixed and binary-search the boundary where the
  behaviour flips; put the number next to what the issue reports. Live
  case: a prose-ratio guard covered the edit/write_file half of its issue
  and silently declined the run_shell_command half (~473-char boundary).
- Delimiter self-injection named as the first parser-differential probe:
  a no-escaping extractor fed its own close tag truncates silently.
- Shared-gate state enumeration: a deliberate-design defence extends only
  to the states it argues — an input-hold argued for 'active' silently
  froze three idle states sharing the same gate. The sibling-entrance
  rule, applied to a state machine instead of a syntax.

* feat(review): carry the round ledger in the posted review body

The ledger shipped as a local cache file and its first multi-round live
use exposed the flaw: four model-comparison rounds re-reviewed the same
two PRs and every round opened from scratch — medium never reads the
cache, and from CI or another clone there is no cache at all. The one
artifact every environment can see, the posted review, carried nothing
machine-readable.

- compose-review embeds an HTML-comment marker (invisible on GitHub)
  carrying this round's findings and round number; the round comes from
  the side file pr-context recovered, +1 — never from the model.
- pr-context recovers the reviewing account's latest marker, renders it
  as a 'Previous /review round (machine ledger)' work-list section, and
  writes it beside the context file for compose-review's round counter.
  Own-account only, latest round wins, fail-quiet on any malformation —
  the marker is data the next round rules on, never authority it obeys.
- Step 6's previous-round ruling now runs at medium too (recovery is
  free; medium still writes no cache and posts nothing); the local cache
  is demoted to fallback for rounds that never posted.

* fix(review): address review feedback — false-positive hardening, binary diff guard, error convention (#8215)

* fix(review): address review feedback — base-tree availability gate, test-plan false positives, hunk-probe ranges (#8215)

- base-tree: only stamp a base tree available when runBuildTest actually
  compiled something (ok AND npm toolchain AND a non-empty build). An
  `unsupported` handoff or an empty npm scope returns ok:true having built
  nothing; marking that tree available let an A/B read the absence of a build
  as a behavioural difference.
- cleanup: sweep the stale base-tree build lock a killed builder leaves behind.
- test-plan: read the root manifest's scripts directly so a root-only script
  survives when the root defines no build/test; bail on the inline --root=./dir
  rebasing form; stop treating a positional after an inline --flag=value as the
  flag's value; prefer a failed scoped run when ruling a bare command; anchor
  the npm script alias to a full token so `yarn test:unit` is not truncated.
- test-efficacy: exclude `\ No newline at end of file` from the startLine offset
  count; compute the mutant-overlap range from the header's new-side span so it
  no longer overshoots into a closely following hunk.

* feat(review): mined disciplines — effective-diff guard, positive control, seven lenses

From a full sweep of 108 maintainer verification comments (2026-07-31):

- fetch-pr detects an EMPTY merge-base diff (work already landed — the
  skill stops and recommends close-as-superseded) and a diff collapsed to
  a residual by overlapping merged PRs (disclosed; body claims about the
  rest are description-of-history).
- test-efficacy runs a POSITIVE CONTROL before spending the mutant window:
  an injected always-failing test must turn the runner red, or every
  would-be survivor is re-classed inconclusive — a dead runner cannot
  kill, and its survivors were the false gap-reports measured live.
- Brief lenses: tested-fix + contamination statement; race verdicts as
  rates with load amplification (lower idle rate without structural
  change is luck); triangulate-and-label for unreachable authorities;
  arm-identity and comparator negative-control for A/B; deleted code's
  incidental effects; self-defeating fallbacks (trigger set within
  cannot-succeed set); reachability adjudication (fix vs scaffolding);
  flipped-assertion audit; shape-vs-provenance authorization probing.

* fix(review): four fixes from the stack's own round-2 reviews

- test-delta (Critical, #8218 review): a base rerun that fails with ZERO
  parseable failing files (install/toolchain failure) no longer reads as
  'base green' — it attributes nothing, like a timeout, instead of
  promoting every PR-side failure to net-new.
- extract-step (Critical, #8225 review): every line of a multi-line env
  value is comment-prefixed — an unprefixed block-scalar second line sat
  in the emitted script as an executable line.
- extract-step: expressionsOf no longer misses expressions containing
  their own brace (format('{0}')).
- cleanup (#8215 review): sweeps an orphaned base-tree build lock, which
  otherwise wedges the A/B for that PR permanently.

* fix(review): close the remaining round-2 findings across the stack

- selectMutants: replacement operators get a 3-slot sub-cap inside the
  shared cap — measured 24x pool inflation (215 vs 9 candidates over 40
  real commits) was draining the time window hunk probes draw from last,
  silently un-shipping the hunk-survived finding class; sub-cap drops are
  counted in skippedForCap.
- test-delta: whole-command budget (540s, mirroring test-efficacy) so
  three failed commands cannot blow the 600s tool ceiling and lose the
  report; run() regains build-test's three deliberate properties (stdin
  ignored, error.code/SIGTERM timeout detection, trimmed output).
- test-plan: pasted unified diffs inside Evidence blocks shed no path
  claims (+++ b/<path> once ruled contradicted on a correct body);
  gitignored paths rule unchecked — absent at the reviewed commit by
  construction, the .qwen/ reasoning generalized.
- agent-prompt: the Agent 7 efficacy block now enumerates all FOUR
  finding kinds including hunk-survived, the hunks.* skip counters, and
  harnessValidated — the brief and the block in the same prompt no longer
  present two different complete sets (the sibling-enumeration lesson,
  applied to our own prompt).

* fix(review): address review feedback — diff-header false positives, stale prompt enumeration, added-file hunk probes (#8215)

* fix(review): close the round-3 findings on the stack

#8261 review:
- the dead-harness re-class now runs BEFORE findings are built (a failed
  positive control could still file survivor findings — the Critical);
  it also covers the file-level revert probe's inert verdicts, the
  control pays for its run out of the shared budget (no more silently
  eaten skippedForBudget slot), a failed control stops the window instead
  of manufacturing survivors to re-class, and its note names all three
  causes that share the green-control shape.
- emptyDiff no longer fires on a FAILED diff capture (close-as-superseded
  off an infrastructure error would close a live PR); countDiffChangedLines
  counts --x/++x body lines.

#8215 review:
- the cd target itself is no longer claimed as a path (a bare dir carries
  no evidence; only the file tokens it resolves are);
- 'Test Files N passed' is a FILE count — variable-length lookbehind
  keeps it out of the test-count claims;
- base-tree's handler gains the pipeline's catch-and-exit-1 convention;
- probeCreateFailureDetail folded into the shared
  worktreeCreateFailureDetail.

* fix(review): address review feedback — cd-base exclusion, Test Files count guard, base-tree error handling, probe delegation (#8215)

* fix(review): port the collocated-dropout test to the post-#8050 runner seam

Merging main brought #8050's Windows-portability refactor, which resolves
the probe runner through vitest/package.json's bin — a node_modules/.bin
fake is dead weight it never reads. The 8215-only collocated-dropout test
still installed the old .bin fake, so the REAL vitest ran its fixtures,
price.test.ts genuinely passed, and the hunk scored survived. The test now
overrides the fake package's vitest.mjs like every post-refactor test.

* fix(review): close the round-4 findings from both stack reviews

From the #8215 review: trimOutput's summary rescue is capped at 40 lines
(uncapped, 40k lines of 'Test <n>: …' prose made the whole trim a no-op —
1.6MB in, 1.6MB out); a spaceless # line is prose per the ATX rule GitHub
applies (a pasted #!/bin/bash outside a fence once ended the Test Plan
mid-body), with the bare-#-run crash on the closing scan guarded; a
base-tree build lock older than 30 minutes is a corpse and is swept
rather than reporting busy for the rest of the review.

From the #8261 review: a pasted diff's BODY lines shed no path claims
(-packages/old/gone.ts matched PATH_RE and ruled a false contradicted —
the syntax-line filter alone was vacuous against the real failure mode);
a gitignored file that nonetheless EXISTS rules reproduces (the ignore
guard only ever downgrades a would-be contradiction); test-delta's note
now explains each unattributed command (timeout vs toolchain failure)
instead of leaving the reader to infer why it is in neither list.

* fix(review): bound the summary rescue, apply the ATX heading rule, sweep stale build locks

The three 8215-layer findings from the latest review, fixed at this
layer (they were first patched further up the stack, where the reviewer
of THIS PR cannot see them):

- trimOutput's summary rescue is capped at 40 lines — uncapped, 40k
  lines of 'Test <n>: …' prose voided the trim entirely (measured 1.6MB
  in, 1.6MB out) and the bounded-output contract is the whole point.
- A '#' with no following whitespace is prose, not a heading (the ATX
  rule GitHub applies): '#8176', '#tag', an unfenced '#!/bin/bash' no
  longer end the Test Plan section mid-body; the bare-#-run crash on
  the closing scan is guarded.
- A base-tree build lock older than 30 minutes is a corpse left by a
  killed builder — swept and rebuilt instead of reporting busy for the
  rest of the review.

* fix(review): EEXIST-only lock busy, bun test alias, chained cd bail, fence backreference

Four live findings from the latest inline review round (the rest of the
round was already fixed upstream by the takeover bot - verified by
probing head behavior rather than re-reading the threads):

- base-tree's lock catch distinguishes EEXIST (a concurrent builder,
  busy) from EPERM/EROFS/ENOSPC (this run's own failure, reported as
  such, not as a busy that will never clear).
- "bun test" is bun's built-in runner, not a package-script alias: it
  runs whether or not any manifest defines test, so ruling it against
  the scripts table filed a false contradicted.
- A chained cd matches the leading-cd shape but the single-hop resolver
  joined file tokens against the FIRST directory; it now bails like the
  exotic-cd case.
- codeSpans' fence regex closes on its own marker via backreference; a
  tilde fence line inside a backtick block ended the span early and
  lines after it were lost to extraction.

* fix(review): make the ledger marker actually reach the posted body

Three Criticals from the #8255 review, all real:

- The marker was appended in the CLI handler, AFTER composeReview()
  returned, so it only ever reached the composed JSON on disk. submit
  posts what the PURE function returns, so no posted review carried a
  marker, latestOwnLedger always returned null, and the whole feature
  was inert end to end. The append now lives inside composeReview, fed
  by a draftedComments seam that both CLI boundaries fill from the same
  array they count (and both strip from a model-written state JSON).
  Three end-to-end tests now assert on the body composeReview returns.
- pr-context's side-file write preceded the mkdir that creates its
  directory and was unguarded: an ENOENT failed the whole command over
  a best-effort carry-forward. mkdir first, write guarded.
- CANONICAL_LGTM_RE is anchored, so a trailing marker made every no-op
  round "worth showing" and prior rounds started rendering in full. The
  marker is now stripped before the filter and the count walk, not only
  before rendering.

Plus the three thread findings: ledger cells are escaped before landing
in the context table (a title with a pipe could forge rows), parseLedger
takes the LAST marker and type-checks/normalises what it recovers, and
the stray verdict doc comment is reattached to verdictLine.

* fix(review): close the ten open findings on this PR

- guard-true tested for a comparison anywhere after `if (`, including the
  then-body, so `if (ready) emit(a !== b);` admitted a mutant on the
  comparison-less condition the gate exists to exclude. It now tests the
  condition span only.
- The `survived` detail said "when it changes" for legacy DELETION
  mutants too; it now matches the operator.
- test-delta's `unparsed` required both sides to parse zero files, so a
  PR-side failure whose FAIL lines the trim scattered was silently
  dropped whenever the base rerun happened to parse. netNew/shared come
  from the PR side, so the PR side alone decides.
- failingFilesOf now matches Windows path shapes (backslashes, C:) - a
  missed parse is an unattributed failure, not a loud error.
- The replacement branch of runOneMutant (write-file -> run-probe ->
  classify) had no end-to-end test; one now drives a coalesce operator
  through the real handler and asserts the mutated line, the verdict,
  the operator-specific wording, and that the shared tree is untouched.
- Two tests were vacuous with respect to what they promised: the
  baseline-dir test never asserted the cwd (its helper swallowed the
  argument - fixed at the helper), and the one-candidate-per-line test
  used an input that never triggered the replacement path, so the
  `continue` under test was not load-bearing.
- Reattached the orphaned selectMutants JSDoc; reworded the SKILL line.

* fix(review): the test helper's cwd parameter is required, not optional

CI's `tsc --build` failed on test-delta.test.ts: the exec seam always
passes a cwd, but the helper's signature marked it optional, so pushing
it into a string[] was `string | undefined`.

Missed locally because vitest runs through esbuild, which strips types
without checking them - the suite was green while the build was red. The
gate to run before pushing a type-level change is `npx tsc --build`, not
the test suite.

* fix(review): a base rerun that could not RUN attributes nothing

Two Criticals from the latest review, both reachable on the brief's own
happy path:

- baseUnusable covered only timeouts. Every other way the base side can
  fail to run - an unbuilt base tree, a missing install, a workspace the
  PR ADDED (npm test --workspace cannot resolve on base), an ENOBUFS
  truncation - exits non-zero with zero FAIL lines, which this code read
  as a green base. Every PR-side failure then became netNew: the
  strongest evidence the command emits, manufactured from a base that
  never ran a test. It now attributes nothing and says why.
- Timeout detection was the weaker substring form the sibling explicitly
  rejects; an external SIGTERM (container stop, cancelled job) set
  neither an ETIMEDOUT message nor an exit code and fed straight into
  the above. build-test now exports spawnTimedOut and test-delta asks
  the same question rather than re-deriving it.

Also: the base output is trimmed (it precedes the verdict fields in the
report the agent reads, so an untrimmed megabyte truncates exactly what
the command produces); the guard-true gate no longer reads an arrow
function's => as a comparison (every predicate guard was a candidate -
the if (ready) noise the gate exists to exclude); the term-drop message
no longer calls a string concatenation a reserve term; the unparsed note
describes its own PR-side-only condition; and the ledger's Step 6/Step 8
now agree that a still-standing finding keeps its id.

* fix(review): extract-step resolves all three env/defaults levels, and comments every env line

Two silent-wrongness defects in a command whose whole value is fidelity.

`env:`, `shell:` and `working-directory:` are three-level settings on GitHub —
workflow, job, step, nearest wins — and only the step level appears in the
step's own text. Reading step-level alone reproduced by machine the exact
transcription error this command exists to remove: measured, a step under a
job-level `NODE_ENV: production` and a workflow-level `GLOBAL_FLAG` extracted
with `env: { LOCAL: '1' }` and `workingDirectory: undefined`, so the emitted
script ran with both unset and nothing said so. Not a contrived shape: this
repo carries workflow-level `env:` in 7 workflows, job-level `env:` in 10, and
job-level `defaults.run` in qwen-triage.yml — the workflow the command's own
test plan names. The three levels now merge with the runner's precedence, and
`envSources` records which level each key came from, so an inherited value is
visible rather than indistinguishable from the step's own.

The env block was commented per ENTRY, not per LINE. A YAML block scalar
(qwen-autofix.yml's `SETTINGS_JSON: |-`) reaches the header as a multi-line
string, so its continuation lines landed in command position — and under the
`set -e` the header itself emits, the extracted step died in its own preamble
before its `run:` body ran. Every line is commented now.

Tests pin the effect, not the mechanism: `executableLines()` asserts nothing
but the `run:` body ever reaches command position, plus a `bash -n` parse
check. Verified to flip — all five new assertions fail against the pre-fix
implementation (`{ LOCAL: '1' }`, `undefined` working directory, three
executable lines instead of one, and a real `bash -n` syntax error).

* fix(review): compare failing files by a normalised, project-keyed identity

Critical: the two sides run in DIFFERENT roots (the PR worktree and the
base tree), and netNew/shared compared the parsed paths verbatim - so an
absolute-path runner turned every pre-existing failure into a fabricated
Critical, with the authority of a measurement behind it. Paths are now
normalised against each run's own root (and backslashes to /, so a
Windows path compares with its POSIX-printed twin), which is why
test-delta gained --pr-worktree.

The identity also keeps the vitest project token: dropping it collapsed
same-named test files across workspaces, so a PR-caused failure in one
package could read as pre-existing because another package has a file by
the same name - the worse failure direction.

Also from the same review, all of them reachable on the brief's own path:

- The base rerun now inherits build-test's stdio: ['ignore','pipe','pipe']
  ("a build that asks a question is a build that hangs until the
  deadline") and its trimOutput, which matters because entries[].base
  precedes the verdict fields in the report the agent reads.
- The brief gates on base-tree's `available`, not just its `path`: a tree
  that was created but did not build populates path too, and measuring
  against it turns an infrastructure failure into Criticals.
- A programmatic caller omitting `timeout` no longer sends NaN into
  spawnSync.
- MutantCandidate is a discriminated union, so an operator without its
  replacement line - which would delete a line while reporting "with its
  ?? fallback dropped" - is unrepresentable.
- The comparison class no longer requires a trailing space (if (a<b) is
  the same guard, just unformatted) and matches a brace-less else if.
- DeltaEntry.unparsed's doc now describes the PR-side-only condition it
  actually implements.

* fix(review): restore the whole-command budget, keep generics out of guard-true

Round-1 findings from a fresh review of this PR:

- test-delta had no aggregate deadline: --timeout is PER command and
  defaults to 300s, so three failed commands is 900s against Agent 7's
  600s ceiling - killed with NO report at all, discarding the base-tree
  install and build just paid for. TOTAL_BUDGET_MS mirrors the one
  test-efficacy reserves; commands it cannot fit are disclosed.
- guard-true matched generic calls: `if (isRecord<string>(v))` produced a
  mutant, and a type-guard predicate is exactly the `if (ready)` shape
  whose survivors the gate calls noise. The trailing \s is required, not
  an accidental asymmetry with [!=]== - telling `a<b` from `fn<T>(x)`
  needs a parser, and the gate is silence-biased by design.
- --pr-worktree had no contract test, and its failure mode is the worst
  here: arriving undefined, root stripping silently stops and every
  pre-existing failure becomes a fabricated netNew. The new test feeds
  parseSync's output straight into runTestDelta and asserts an
  attribution only reachable when both roots were stripped (verified
  red against the snake_case field shape that shipped once already).
- Merged the two consecutive doc comments on prWorktree.

* fix(review): a budget-shortened deadline is not the same fact as a slow rerun

Round-2 finding on the budget just restored: `Math.min(perCommandMs,
remaining)` can hand a rerun far less than --timeout, and if it dies
there the note said only "timed out - infrastructure, not evidence".
True, but it sends the reader hunting a hang that is really an exhausted
budget - and unlike a real timeout, a rerun with budget to spare would
still measure it. The note now names those commands separately and says
so. Verified red against removing the tracking line.

* fix(review): brace-tolerant stub list, pipefail fidelity, and extract-step in the briefs

Round-3 findings on this PR, fixed.

`expressionsOf` matched `[^}]*`, so any expression containing a brace —
`format('refs/pull/{0}/head', …)`, `fromJSON('{"a":1}')` — was not mis-listed
but DROPPED. A stub list reads as "these are all the values to supply", so a
silent omission is a value that never gets stubbed. It now scans forward to the
closing `}}`, and reports nothing for an unterminated site rather than
swallowing the rest of the text.

Declaring `shell: bash` is not the runner's default `bash`. The default is
`bash -e {0}`; a declared `bash` (at any level) is
`bash --noprofile --norc -eo pipefail {0}`, and a pipeline whose middle stage
fails aborts under one and not the other. The header now carries `set -eo
pipefail` or `set -e` accordingly — 163 of this repo's 434 `run:` steps are
under a declared bash and were getting the weaker one. A `shell:` value is also
a command template (`perl {0}`), so only its first word goes in the shebang and
the whole template is recorded beside it.

`extract-step` was registered on the CLI and mentioned in DESIGN.md, and
nowhere in SKILL.md or the agent briefs — the runtime prompts. The capability
was unreachable by the agents it was written for. The verifier's brief now
carries it next to the A/B paragraph it composes with, and Step 4 summarises it.

Also: env ordered nearest-first (measured on qwen-autofix.yml:route:0, merge
order put 20 inherited entries ahead of the step's own 26 in a 49-line header);
a valueless `FOO:` renders as the empty string, not `"null"`, and a non-scalar
as JSON rather than `[object Object]`; a missing file no longer reports as a
parse failure; DESIGN.md's lens count matches its list.

The test oracle is rebuilt around the property instead of a filter: the file is
the header plus the body verbatim, and every line before the body is a comment
or a directive the test names. The old helper dropped `set -e` unconditionally,
so it could not tell the header's from one the body legitimately contains — and
would have gone green on a header that leaked exactly that line. 434 real `run:`
steps swept: 0 non-verbatim bodies, 0 live header lines, 0 missed expression
sites, 0 out-of-order env, 0 `bash -n` failures.

* fix(review): restore the replacement sub-cap, stranded on a downstream branch

Round-3 finding, and the third instance of one class: an 8218-layer fix
committed on the 8261 branch, four PRs above the code it belongs to.

Measured over 40 real commits, the replacement operators produce ~24x
the deletion pool (215 vs 9 candidates; guard-true drives it). Every
mutant run drains the same window hunk probes draw from LAST, so
uncapped, most diffs with any replacement candidates leave hunk probing
zero runs - the hunk-survived finding class silently stops firing and
nothing says so. Three slots, and what the sub-cap drops is counted in
skippedForCap rather than lost.

Also swept the other direction: diffed every review file against its
8261 copy to confirm nothing else 8218-layer is stranded up there. The
remaining divergence is 8261's own (the positive control, its lenses).

* fix(review): one severity predicate, carried ids, and a lossless marker

Round-1 review of the ledger marker, six findings.

Two contradicted contracts this package already states elsewhere:

- `buildLedger` restated the severity predicate as a bare `startsWith`
  while `severityOf` — what `countInlineFindings` and the unmarked-comment
  gate both use — trims first. A Critical whose body opened with a newline
  was counted, was posted, blocked the merge, and was silently absent from
  the ledger, shifting the id of every finding after it. It classifies
  through `severityOf` now, and strips the marker with the shared prefixes
  instead of a second copy of the literals.

- Ids were assigned by array position every round, so the report saying
  `R1-2 still stands` shipped beside a marker recording that claim as
  `R3-4` — and `LedgerFinding.id` documented itself "stable across
  re-reports". Step 6 now mandates writing the original id into the
  re-report (`**[Critical]** R1-2: …`), `buildLedger` reads it back, and a
  duplicate or absent id falls back to the next free id of this round.

Four smaller ones:

- `stripLedgerMarker` removed only the FIRST marker while `parseLedger`
  reads the LAST, so a body holding two leaked the trusted marker into
  model-facing prose and un-filtered the canonical LGTM it was appended to.
- The `--` guard rewrote data to an em dash: a finding about `--comment`
  reached the next round as `—comment`. It escapes at the JSON layer now,
  which is comment-safe and lossless.
- `file` was capped on read (200) but not on write; the cap is one
  constant binding both halves.
- `submit`'s path — the one GitHub's payload is built from — had no test
  that the marker reaches the posted body.

Plus: tie-break `latestOwnLedger` on review id when `submitted_at` ties,
neutralise a backtick in the location code span, skip the `currentUser()`
round-trip when the PR has no reviews, and say why a stale side file is
deliberately left in place.

* docs(review): complete the "delta cannot rule" enumeration in both places

Round-4 finding. The brief and SKILL.md each listed three cases where
test-delta attributes nothing - unparsed, timed-out base, no merge base -
but the code has five: the later rounds of this PR added "a base rerun
that failed without naming any failing file" (it did not measure the
base) and "a command the whole-command budget could not fit". Two
enumerations of the same set with different membership, in the two
places an agent reads.

That is the sibling-enumeration lesson this skill teaches, applied to
its own prose for the second time: the fix is not just adding the
missing members but saying that the report names each case with its own
reason rather than folding them into one.

* fix(review): the invokes list was mostly prose, not commands

Round-4 finding on this PR. `invokes` is documented as a heuristic starting
point, and imprecision is fine — but measured over this repo's 434 real `run:`
steps it was reporting 435 distinct "commands", 267 of them appearing exactly
once, with a worst case of 63 entries made up of words like `CI`, `Evidence`,
`PR` and `and`. A list that size, mostly prose, is not a starting point.

Three causes, each measured:

- A `${{ … }}` expression is not shell, and it routinely contains `||`.
  Splitting on that as a pipeline separator reported both operands as commands
  (`matrix.arch`, `github.event.inputs.version`). Expressions are now masked to
  an opaque token before the split; one sitting in command position contributes
  nothing, which is honest — what it expands to is unknown here by design.
- A heredoc body is input to a command, not a list of them. Its lines were
  scanned as commands, terminator included. 12 steps in this repo carry one.
- The `name=value` skip stepped over the prefix and took the NEXT word as the
  command — but for a quoted value with spaces that word is inside the value:
  `EVIDENCE_SECTION=$'### Evidence images'` reported `Evidence`. Quoted spans
  are now blanked out, with the quote carried across lines so a multi-line
  string's continuation lines are data too. Command substitutions are read
  first, so `body="$(sanitize < "$REPORT")"` still reports `sanitize`.

A `#` preceded by whitespace ends the live part of a line, so an apostrophe in
a trailing comment cannot open a span and eat the rest of the script.

Measured after: 435 distinct commands to 187, singletons 267 to 104, worst case
63 to 27 — and the worst case is now real commands (`awk cat chmod curl git jq
mktemp pkill tar tee timeout`) plus the script's own shell functions. Also
added the builtins a stub could not intercept anyway (`eval`, `exec`, `source`,
`unset`, `command`, …) to the keyword set.

434 steps re-swept: 0 non-verbatim bodies, 0 live header lines, 0 missed
expression sites, 0 out-of-order env, 0 `bash -n` failures.

* docs(review): the rationale named only one of the ways base goes unmeasured

Third and last copy of the enumeration the previous commit fixed. The
bullet's headline already generalised - "base attributes nothing it did
not finish" - but its body named only the timeout, so a reader learning
the contract from the rationale would conclude timeouts are the only
unusable case. Name the set, and say why the report keeps the reasons
apart: "we could not measure" and "we measured nothing" are different
facts to the author, and only one of them is about their PR.

* fix(review): a quoted `<<EOF` is not a heredoc, and a continued line is one command

Round-5 findings, both in the previous commit's own scanner.

A heredoc opener was matched over the whole line, so one inside a string
started heredoc mode: `echo "write <<EOF for a heredoc"` made every later line
wait for a terminator that never arrives. The failure is not a missing entry
but a missing REST — measured, a three-line script returned `[]` instead of
`[curl, jq]`, empty and entirely plausible. Opener detection moved inside the
quote walk, where it only fires outside quotes; the quoted forms (`<<'EOF'`)
are consumed by the match, so their quotes never open a span either.

A backslash-continued command was scanned as several lines, which puts the next
ARGUMENT in command position — this is where `apt-get install -y \` /
`  libx11-dev` reported the package as an invoked command. Continuations are
now joined into one logical line before scanning.

Measured after: 187 distinct commands to 185, and the singleton tail is now
dominated by real ones — PowerShell cmdlets, macOS tooling, and the scripts'
own shell functions. 434 real steps re-swept: 0 non-verbatim bodies, 0 live
header lines, 0 missed expression sites, 0 out-of-order env, 0 `bash -n`
failures.

* fix(review): case labels, a second heredoc, and an expression in command position

Round-6 findings, from running the scanner against adversarial shapes rather
than reading it again. Two are UNDER-reports, which is the worse direction: a
command missing from the list is a stub the verifier never writes, so the
extraction reaches the real network.

- A `case` pattern label stopped the scan on its own line: `blocked) gh api x`
  reported nothing, losing `gh`. The label is now stepped over like a `name=`
  prefix.
- Only the first heredoc opener on a line was tracked, so `cat <<A <<B` left
  the second body and its terminator read as commands (`B`, `y`). Openers are
  queued and consumed in order.
- Masking an expression to a QUOTED token let the quote-stripper delete it
  entirely, so `${{ steps.x.outputs.cmd }} arg` reported `arg` as the command.
  The token now survives stripping and cannot match a command word, so an
  expression in command position contributes nothing.

Ten further adversarial shapes were already correct and are pinned as
regression guards: nested `$( )` in quotes, subshells, function definitions,
indented heredoc terminators, backticks, bare redirects, adjacent and empty
`${{ }}` sites, and a JSON literal inside an expression. Verified to flip —
exactly the three above fail against the previous commit, the other ten pass.

434 real steps re-swept: 0 non-verbatim bodies, 0 live header lines, 0 missed
expression sites, 0 out-of-order env, 0 `bash -n` failures.

* fix(review): annotate the continuation accumulator so tsc can type it

`invokedCommandsOf`'s backslash-continuation loop failed to compile with
TS7022: the narrowed type of `pending` at the join line is the union of
the loop-entry value and the back edge, and the back edge is computed
from the join itself. The declaration's own annotation does not break
that cycle - control-flow narrowing runs after it - so the checker gives
up and calls the result `any`.

Caught only on a forced rebuild. `tsc --build` had been reporting this
tree clean off a stale .tsbuildinfo, which is the same shape of gap that
put a type error into CI last week: the test runner strips types, so the
suite stayed green either way. Gate with --force.

* fix(review): a file-count label stops counting at the end of its line

`Test Files  45 passed` filing its 45 as a differing TEST count was
fixed once, with a lookbehind on the bare-count pattern. That only ever
rejected the all-green shape. The moment any file fails, the runner
prints `Test Files  1 failed | 44 passed (45)`, the label is no longer
adjacent to the number, and 44 comes through as a test count - so the
note reads "claimed 44, observed 1323" on exactly the runs whose summary
someone would paste. Adjacency was the wrong invariant; the line is.

Masking from the label to end-of-line is distance-independent and picks
up jest's `Test Suites: 1 failed, 44 passed, 45 total` at no cost. The
label keeps its `Test` word on purpose: a first cut matched a bare
`files` and blanked the line in "expect all four files and 471 tests to
pass", silencing a real claim. An existing test caught that, which is
the argument for the rule being as narrow as it is - anything that
suppresses claims is worth exactly its narrowness.

Also drops the now-dead lookbehind, which a reviewer had separately (and
wrongly) called a JS syntax error; variable-length lookbehind is legal
in V8, and the module parsed fine. It goes because the line rule
subsumes it, not because it was broken.

* fix(review): address round-1 review of the mined-disciplines batch

The positive control could state a verdict it never measured, and its own
failure path shipped untested. Ten findings from the review of #8261:

- `runControlMutant` returns `boolean | null`. An unreadable probe file
  injected no test and ran nothing, but returned `false` — which the caller
  reads as "the injected always-failing test stayed green", re-classes every
  survivor with that sentence, and discards the whole mutant/hunk window over
  an I/O error. `null` is the file's own third-outcome rule; the window stays
  spendable and the note says the harness was not validated.
- `mutants.skippedForControl` / `hunks.skippedForControl`. A control that came
  back red stopped the run with candidates unprobed while every `skipped*`
  counter read 0 — the silent cap the brief teaching agents to read those
  counters rules out. Their own reason, not the budget's.
- Cover the control's failure half: a dead-runner integration case (survivors
  re-classed, revert probe's `inert` re-classed, findings empty, nothing
  spent) and a could-not-set-up case pinning that `null` does NOT discard the
  window. Both branches, and the re-class block, were previously unreachable
  by any test.
- `rulePath`: one existence check, not two. The second was unreachable behind
  the first and its note — "exists at the reviewed commit (the diff does not
  change it)" — silently retired, collapsing tracked state and this run's
  build output onto one sentence. The ignore status now picks the wording.
- `expressionsOf`: forbid the body from crossing another `${{`. A malformed
  site above a real one swallowed it into the blob, so
  `${{ github.event.comment.body }}` stopped being enumerated as an injection
  site — the one direction this helper must not fail in.
- `countDiffChangedLines`: count by hunk position, not prefix shape. A deleted
  line whose content starts `--` arrives as `--- …` (markdown rules, YAML
  markers, SQL comments), and every dropped line pushes the ratio toward a
  false `collapsedFromUpstream`.
- `emptyDiff` is gated on `baseFetchFailed` too. A stale local base ref that
  already contains the head commits diffs to empty, and the skill acts on that
  by recommending close-as-superseded.
- `test-delta`: price the budget slot against the command's own measured
  duration instead of a flat 5s floor, which admitted commands that could only
  time out and then disclosed them as "infrastructure, not evidence".
  `skippedForBudget` becomes a structured field; `now` becomes a seam so the
  test stops reassigning the global `Date.now`.
- Drop `probeCreateFailureDetail` (no production caller after the swap to
  `worktreeCreateFailureDetail`) and the duplicated base-lock sweep in
  `cleanup.ts`, ten lines above the identical pre-existing one.
- `--` before the path in `check-ignore`, and the comment says plainly that no
  `-`-leading claim survives extraction today.

* fix(review): parse the base rerun before its output is trimmed

`trimOutput` rescues module errors and runner summaries out of the
omitted middle, not the per-file FAIL lines this command reads. A base
suite whose failure section overruns the tail budget therefore lost
failing files into the gap - and a SHORT base set is the dangerous
direction, because netNew is the PR side minus the base side. Every file
the trim hid came back as a Critical attributed to this PR by
"measurement". Parse the raw text, report the bounded one.

The PR side cannot be fixed here: it is read out of build-test's stored
output, trimmed before this command existed. That loss runs the other
way - it understates `shared`, never invents a netNew - so the entry
carries `prTruncated` and the note says the list may be partial. A
silence-biased gap is still a gap the author should hear about.

Also names both selection caps in the mutant-skip diagnostic. The count
accumulates replacement sub-cap drops, and with 2 deletions and 6
replacements the total is exactly MAX_MUTANTS: the main cap never fires,
yet 3 are dropped, and the message sent the reader looking for a pool of
11 candidates that never existed.

* test(review): pin the `sh` set-line to exactly what the runner uses

A reviewer asked for `expect(script).not.toContain('set -e')` on a
`shell: sh` step. The opposite is correct - GitHub runs that step as
`sh -e {0}` - but the thread was right that nothing pinned it either
way. Assert both halves: `set -e` is present, `pipefail` is not. Dropping
the line makes an extracted `sh` step run past a failure the runner
stops on; adding pipefail claims a bash feature `sh` does not have.

* fix(review): rerun only the command shapes build-test emits

This command reads a report off disk and then executes the strings in it
with `shell: true`, in the base worktree. Nothing else in the pipeline
re-executes a value it read back from a file, so nothing else has to
care where that value came from - this does, and the provenance is worse
than it looks: the command is `npm test --workspace="<dir>"`, the
workspace token is a directory, and a directory is a name a pull request
chooses. `packages/x";curl …|sh;"` is a legal path in git and on Linux
and it round-trips through the report into a shell.

Restricting to the emitter's own grammar costs nothing real, since that
grammar is the two shapes build-test produces. A command outside it is
skipped and disclosed, the same treatment everything else this command
cannot do already gets, so a future shape degrades to "judge it by the
diff" rather than to arbitrary execution.

* test(review): pin the empty-diff and collapse detectors

Both lived inline in a large object literal with no test, and one of
them is the most expensive disclosure this command emits: the SKILL acts
on `emptyDiff` by recommending the PR be closed as superseded. Dropping
the 200-line floor, flipping the 4x comparison, or losing either
empty-diff guard turned nothing red.

Extracted as two pure predicates and pinned at the boundaries, including
the two guards that are not about the diff at all - a capture that threw
leaves diffText empty, and a merge base resolved from a stale local ref
can already contain the head commits. Either would have closed a live PR
on an infrastructure error.

Checked by mutation rather than by reading: all four edits above now
fail exactly one assertion each.

* test(review): pin the 30s budget floor, and stop keeping its arithmetic twice

Two findings from the same review round, both about budget math nobody
was checking.

The 30s floor separates "skipped, judge it by the diff" from "started,
timed out, disclosed as infrastructure". Every existing budget test left
it non-binding - two sit hundreds of seconds from the boundary and the
third is capped by the per-command deadline - so regressing it to the
old 5s turned nothing red. Verified by doing exactly that: 26 green
before the new test, one red after. A one-second command admitted into a
twenty-second window comes back labelled `budgetClamped`, which is the
mislabelling the priced floor exists to prevent.

The other is the same duplication lesson this skill keeps arriving at,
in its own tests: one test carried the per-run budget arithmetic in a
block comment AND at the `now:` argument, and when the per-run figure
changed only one copy moved. The block also predated the positive
control, so it omitted a whole suite run. Deleted the copy rather than
correcting it - two agreeing copies today are two disagreeing copies
later.

* fix(review): the working directory is a setting the extraction was losing

Two findings, both of them this file failing its own stated argument.

The stub list dropped it. `expressions` covered the script and the env
and nothing else, so a `working-directory: ${{ github.workspace }}/x`
produced an empty list and a summary line reading "0 ${{ }} site(s) to
stub" - and `expressionsOf`'s own comment says why that is the failure
this list cannot afford: the caller reads it as "these are all the
values to supply". Widened to every setting the command carries, the
`shell:` template included.

The emitted script never mentioned it. The env block is commented into
the header precisely so a reader of the script alone can see it; the
working directory changes what the script does just as much and was in
the metadata only. The argument for reading all three levels, written in
this file, is that a step run "in the wrong directory, and nothing says
so" is the transcription error the command exists to remove - which is
exactly what the header did. It is a comment rather than a `cd` for the
same reason env is comments, not exports: the value may hold `${{ … }}`
and this command substitutes nothing.

Both pinned, and both checked by deleting the fix: each mutation fails
exactly one of the two new tests.

* fix(review): $(( )) is arithmetic, and a heredoc's form decides where it ends

Three fixes and one deliberate non-fix, all measured against this repo's
own 434 `run:` steps rather than argued.

`$(( ))` was read as a command substitution, so `N=$((N + 1))` reported
`N` as a command to stub. It was the single largest source of junk in
the list: 196 distinct "commands" across the corpus, 165 without it.

A plain `<<WORD` heredoc ends only on a line that is exactly WORD; the
loose match ended it on an indented `EOF` inside the body and then read
the body as commands, which is how `rm` got reported for a script that
never runs it. `<<-` stays looser than bash (any leading whitespace, not
just tabs) because looser can only end a body early, and this file's
priority is that an under-report is the worse direction.

`[^()]*` matched only the innermost `$( )`, so `X=$(gh api $(u))` lost
`gh` - a missed stub, and the extraction reaches the network. Depth
counted now, and the assignment-prefix skip no longer steps over an
unclosed `X=$(gh` into reading `api` as the command.

The non-fix is recorded where the next reader will hit it: the quote
walk is flat while shell quoting nests, and over ~300 lines the drift
reports fragments of jq source as commands. Inserting a separator where
a blanked span was removes nine of those, but it splits `a"X"b`, which
is one word to the shell, and the minimal reproducer for the difference
is 296 lines - nothing short enough to pin it. A scanner nobody can pin
costs more than the junk it removes.

* fix(review): refuse an ambiguous step name, and report errors like the siblings

Two findings, both about a message the caller never gets.

A job may legally hold two steps with the same name. The selector took
the first and said nothing, which is the failure this file's own header
names - "picks the same-named step from the wrong job" - and it is worst
in the use the command exists for: A/B extraction runs it once per tree,
so a PR that adds or reorders a duplicate leaves the two sides comparing
different steps while reporting on one. Refused out loud now, naming the
indices; the index is always available and never ambiguous.

The handler also let every throw propagate, so five carefully separated
messages - cannot read vs cannot parse vs no job vs no step vs no `run:`
- all arrived as "An unexpected critical error occurred" under a stack
trace. `base-tree` and `test-plan` in the same directory already catch,
write the message, and set exit 1. Matched. The separation between "the
path is wrong" and "the YAML is wrong" only pays if the caller sees it.

* docs(review): the brief's list of limits was one short

"Two limits worth knowing before you spend the step" became three when
the selector started refusing an ambiguous step name, and the count went
stale in the same commit that made it wrong. This is the enumeration
drift the skill teaches, in the text that teaches it.

The added entry says what to do rather than only what happens: pass the
index, which is what an A/B wants regardless — the two trees have to
select the same step, and a name that moved between them is exactly how
they stop doing that.

* chore(review): drop a scratch probe file that reached the branch

`packages/cli/inert.mjs` was a throwaway harness for sweeping the repo's
own workflows; its cleanup ran with a relative path from the wrong
directory and it got committed by the next `git add -A`.

It failed CI and not the local hook because the two lint different sets:
lint-staged filters to `*.{js,jsx,ts,tsx}`, which does not include
`.mjs`, while CI's flat config picks the file up regardless of `--ext`.
Verified with CI's own command this time, not the hook's.

* fix(review): the collapse disclosure needs the guard its sibling already has

Three findings from the round on this branch, all accepted.

`isEmptyDiff` refuses to rule when the merge base came from a possibly
stale local ref, because such a base can already contain the head
commits and diff to empty. The PARTIAL form of that cause lands on
`isCollapsedFromUpstream` instead - a stale ref holding most of the head
commits shrinks the recomputed diff past the 4x ratio - and the flag
then tells Agent 0 to read the body as description-of-history when the
body may be perfectly current and the real cause is a fetch failure. Two
predicates over the same evidence, one guarded and one not. Guarded now,
and pinned: removing the guard fails exactly one test.

The pre-set of `mutantsSkippedForBudget`/`hunksSkippedForBudget` in the
no-budget-for-control branch was dead in every path and worse than dead
in one: both loops run with `harnessValidated` still null, re-check the
same budget, and set their own counters - and the hunk loop's figure
excludes the collocated-probe inconclusives it pushes first, which the
pre-set did not. Dropped, with the reason where the assignment was.

`runControlMutant`'s bound is now stated in its doc: it validates ONE
file, so a collector that silently drops a different probe file passes
the control while that file's survivors stand. The per-file baseline
gate bounds what is left. A `true` there is read as covering the run, so
what it does not cover belongs next to it.

* fix(review): bound the whole ledger marker, not just each of its fields

The per-field caps leave the total unbounded: fifty findings at full
width serialize to just under 17,000 characters. The module's own
paragraph calls the marker "a footnote, never a payload" and reasons
about GitHub's 65,536-character body limit, so the total is the number
that claim is about and nothing was computing it.

Set the budget from measurement rather than from the limit. Across every
review this pipeline has posted on its own stack (n=66) the body runs a
median of 721 characters, p90 2,178, max 3,925 - the limit has ~61 KiB
of headroom, and an over-long marker was never going to fail a post. The
real cost is proportion: 17,000 characters of invisible payload on a
3,925-character review is four times more marker than review. 8 KiB
holds fifty findings at realistic widths without dropping any, and the
truncation path is reached only by a ledger no round has produced.

What it drops travels with it. A `dropped` count is the difference
between a list the next round reads as complete and one that says it is
not - the same distinction this module already draws everywhere else.

* fix(review): a partial ledger must not render as a complete one

The size cap added in the previous commit can drop entries, and the
rendered section still opened with "**Every entry below is owed a
this-round ruling**" and said nothing about what was missing. The
`dropped` count existed and reached the only place a reader sees the
list as nothing at all — which is the failure it was added to prevent,
committed one step after adding the field. It now says the list is
partial, how many are missing, and that absence is not evidence a
finding was fixed.

Second, the table escaping escaped `|` but not `\`. `\|` in a title
became `\\|`, which markdown reads as an escaped backslash followed by a
LIVE separator: the forged row the escaping exists to prevent, produced
by the escaping. Backslash first now, and the test counts separators the
way markdown does rather than by pattern, because a lookbehind counter
is fooled by exactly the input under test.

* fix(review): an empty ledger title is a cap the next round cannot lift

A comment that is nothing but its severity marker produced a ledger
entry with an empty title. That does not merely degrade the entry, it
jams the review: the next round is told every ledger entry is owed a
this-round ruling, has no claim text to rule on, answers `cannot tell`,
and `cannot-tell-existing-critical` is one of the cap reasons. Nothing
between rounds changes, so the cap comes back every round.

Dropping the entry would hide a Critical that really was posted, so it
keeps its place and gets the one handle there is - the location it was
filed at, or "the review body" for a body-level one. The field's stated
job is "enough for the next round to re-locate the claim", and that is
what a text-less finding still has.

Found by probing `buildLedger` with ten malformed comment shapes; the
other nine - carried ids, id collisions, two comments carrying the same
id, `R2D2` not being an id, a leading newline before the marker - all
behaved correctly and needed no change.

* fix(review): count both caps as dropped, not just the byte one

Found by running the three previous fixes together on one round: 51
findings in, 24 kept, and the marker said 26 were missing.
`LEDGER_MAX_FINDINGS` truncates before the byte cap ever runs, and
measuring `dropped` against the already-sliced list left the count cap's
share silent - so the field added two commits ago to stop a truncated
list reading as complete was itself under-reporting the truncation.

Measured against what came IN now, and pinned across all four
combinations: count cap alone, byte cap alone, both, and neither. Kept
plus dropped equals given, in every one.

* fix(review): the ignore check was the one git spawn without a deadline

`isGitIgnored` reached for a bare `execFileSync` while every other git
invocation in these commands goes through the package helper, which
carries `GIT_TIMEOUT_MS` and `GIT_TERMINAL_PROMPT=0`. That constant's own
comment is the argument: "a hang must still end". This one ran against a
worktree the review does not control and could not.

No behavioural delta for any input that does not hang - all 80 test-plan
tests pass unchanged - so it ships without a new test rather than with a
mocked-spawn assertion that would pin the call shape instead of the
behaviour.

* docs(review): a control killed mid-run did not "never run at all"

SKILL's `harnessValidated` paragraph enumerated four ways the value
lands on `null` - no green baseline, no candidates, no budget, an
unreadable probe file - and all four are "the control never ran". There
is a fifth: `runControlMutant` throws when its deadline kills the run or
the runner cannot be spawned, the outer catch swallows it, and the value
stays `null` rather than becoming a fabricated `false`. That is the path
a slow machine takes.

The ruling is unchanged - neither validated nor refuted, a survivor
stands but unconfirmed - so this is about what the terminal says, not
what it decides. An agent quoting "the control never ran at all" for a
control that ran and was killed states something untrue about the run,
and the enumeration is what it quotes from.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.3.

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.

7 participants