qwen-code/packages/cli
Shaojin Wen 33aee1b7e1
fix(review): stop dropping live blockers, and probe whether new tests actually gate new code (#6790)
* fix(review): stop rendering a maintainer's blocker as an endorsement

The Step 6 re-check exists to stop a review submitting `C=0` while a live
blocker still stands on the PR. On #6486 it did exactly that, and the reason
was structural rather than a lapse of judgment.

`pr-context` quarantined a thread into the mandatory re-check section only if
its body contained the literal string `[Critical]` — a marker only /review
itself emits. A maintainer built the PR, drove the real CLI, and filed
"Finding 1 — Ctrl+F dual-fires ... (blocker)" as an ISSUE comment. Every issue
comment settled into "Already discussed — do NOT re-report" as a 240-character
snippet, and the first 240 characters of that one were its preamble: "I built
this PR from source and drove the real CLI ... to validate the model-toggle
hotkey before merge." That reads as an ENDORSEMENT. The blocker began 1143
characters past the cut. Three hours later /review reviewed the same commit —
the fix did not land until that evening — and submitted "no blockers".

Recognition is now semantic (`carriesBlockerSignal`) and matches assertion
patterns rather than word presence, with a negation guard. Blocker-bearing
bodies — inline threads and issue comments alike — are promoted into a
"Blockers to re-check" section and rendered in full.

Word presence was the first cut and it does not survive contact with a real
thread: on the live #6486 discussion it promoted 8 of 15 issue comments, of
which one was a live blocker. The rest were the triage bot's own template line
"No critical blockers." (the word inside its own negation), the author's
"### Critical fixes" heading, and a comment quoting `[Critical]` while arguing
a finding away. Eight full bodies took the context file from 30 KB to 59 KB and
pushed the real blocker to character 43094 — past the 25000 one `read_file`
returns. The section held the right blocker and no agent could read it, which
is PR #5738's failure reintroduced one section further down.

So the section is written FIRST, ahead of the description and the review
history: nothing in this file outranks the claims a `C=0` verdict may not be
reached without ruling on. On the live thread that moved the heading from
character 25961 to 569 and the blocker body from 43094 to 4421. A character
budget bounds the section even so; bodies past it degrade to snippets naming
their exact fetch, which the re-check already must run before ruling.

The fixture is the real comment body, byte for byte, so the regression is
pinned against the thread that produced it.

* fix(review): make "fixed by this diff" a verdict that has to be earned

The Step 6 re-check has three verdicts, and until now only two of them cost
anything:

  still stands       REQUEST_CHANGES — blocks the merge
  cannot tell        serialized into the body, caps the event at COMMENT
  fixed by this diff nothing. Silent, free, unrecorded.

An agent choosing among three answers where one is free and two are not drifts
toward the free one — and the free one is the only one that can ship a bug.

The bar for it also read "you read the lines and the fix is there", which
invites reading the diff's lines. That is precisely the reading that fails: a
fix's new lines are always in the diff, but whether they WORK routinely depends
on code outside it.

#6486 is the case. The author answered a Ctrl+F dual-fire blocker by adding a
guard to the toggle handler — visible in the diff, and it reads like a fix. It
changed nothing. The second handler is text-buffer.ts:2663, in a file the PR
never touches, subscribed independently to a KeypressContext.broadcast() with
no stop-propagation; returning from one subscriber does not stop the other.
Read the diff and you see a guard and rule "fixed". Read text-buffer.ts:2663
and you cannot.

Determinism owns the evidence, judgment owns the ruling:

- pr-context extracts the evidence. A blocker's body names the code it is about
  — #6486's named text-buffer.ts:2663 outright — so every promoted blocker now
  renders a "Referenced code" list. "Go read the untouched code" stops being a
  hope the agent might have and becomes a list it is handed.
- SKILL.md raises the bar on the ruling: name the mechanism, name what now
  stops it, and when the stopping condition lives outside the diff, read it
  there — or the verdict is `cannot tell`.

No new compose-review input: `cannot tell` already caps the event. The change
is to make wrong "fixed" rulings land there instead of passing silently.

* fix(review): a skipped CI check is not a passing CI check

GitHub reports a skipped job as `status: completed, conclusion: skipped`. The
classifier tested for failure conclusions and for pending statuses, and
`skipped` matched neither — so it fell through both branches and landed the run
in `all_pass`. A job that never ran was scored as a job that passed.

This is load-bearing. /review treats green CI as its licence to approve, and the
whole design delegates runtime truth to CI precisely because the LLM pipeline
reads code statically. If the delegation returns nothing and returns it wearing
a green badge, the delegation is worse than not having it.

On #6486 the one job that would have exercised the new hotkey — "Integration
Tests (CLI, No Sandbox)" — was skipped, as were the macOS and Windows Test legs.
all_pass.

"Did it run" is a question about the check NAME, not about any single run: this
repo's routing workflows (authorize, review-pr, precheck-pr) routinely emit both
a skipped and a successful run of the same name, and reporting those as unrun
would bury the one skipped check that matters under a dozen that do not. A name
counts as executed if any of its runs reached a real conclusion.

Two deliberately different consequences:

- Some checks skipped -> a disclosure, not a downgrade. A docs-only PR
  legitimately skips the test matrix, and auto-downgrading on any skip would
  downgrade every review in this repo, which is how a gate gets ignored. So
  presubmit names them and Step 7 rules on them — whether a skipped check would
  have exercised THIS diff is a question about the diff, which presubmit cannot
  see and the reviewer can.
- Every check skipped -> a downgrade. Checks exist, not one ran: there is no
  green here to approve on, and no judgment is required to say so. A repo with
  no CI at all is a different claim (totalChecks === 0) and is not downgraded.

The check-run shapes in the tests are the real ones from 6486's head commit.

* feat(review): add a test-efficacy probe — does the new test gate the new code?

Agent 5 asks whether a test EXISTS and whether its assertions look like they
check something. Agent 7 runs the suite and reports that it is GREEN. Neither
can see a test that protects nothing, and there are two ways to ship one:

- unreachable — the project's test command never collects the file.
- inert — it runs, it passes, and it would still pass with the change reverted.

#6486 shipped both, in one file. The new test lived in integration-tests/, which
is not an npm workspace, so `npm test --workspaces` never collected it; its CI
job was skipped, so CI never ran it either. The test executed nowhere — not in
CI, not in the review — and nothing in the pipeline noticed. Had it run it would
have passed anyway: it drove a kitty CSI-u sequence into a PTY that never
negotiated the kitty protocol, so the keypress was discarded before reaching the
handler under test. It could only ever have caught a startup crash. Agent 5 saw
a test file with plausible assertions and called coverage fine.

Both questions are decidable without judgment, which is why they are a
subcommand and not a prompt. Unreachability needs no execution at all — a path
against the root package.json workspace globs. Inertness needs one run: revert
the diff's source files to base, keep its tests, re-run them.

The classifier is asymmetric on purpose. Reverting source frequently breaks a
test's own compile — it imports a symbol the diff introduced — and the runner
exits non-zero having collected nothing. Scoring that as "the test caught the
revert" would hand back exactly the false assurance this command exists to
remove. `gated` therefore requires a real ASSERTION failure; a bare non-zero
exit with nothing collected is `inconclusive`, and `inconclusive` is never
reported as a finding.

Verdicts are per test FILE, not per run. One `vitest run` covers every probe,
and a run-level verdict lets one honest test cover for a useless one: the gating
test fails, the run reports failures, and the inert test beside it is scored
`gated` too — so every inert test with a working sibling would be invisible,
which is the exact defect this command exists to find. Found by running it
against a real repo; the unit tests for the run-level classifier all passed.

Two other limits are deliberate: a test-only diff is never probed (a new test for
old code is SUPPOSED to pass with nothing reverted, and flagging it would be a
false blocker on exactly the PRs we want people to write), and findings are
Suggestions, not Criticals — a test that does not gate is not itself wrong code;
what the finding must name is the behaviour now shipping unprotected.

Driven against real PRs: #6433 reports GATED (9 assertions fail on revert, no
finding); #6486 reports its integration test unreachable and its two unit tests
gated.

* fix(review): harden CI classification, path safety, and probe robustness

Five correctness fixes surfaced by a Codex $qreview pass on this PR. Each was
verified against the real code before applying; the review filed 30 Criticals,
of which these are the ones that actually reproduce a wrong result.

- presubmit: paginate `check-runs`. The single-page `ghApi` call saw only the
  first 30 runs — this PR's own head has 508 — so a failing or skipped job past
  the cut was invisible and could let a review approve past it. New
  `ghApiAllNested` streams `--paginate --jq '.check_runs[]'` as NDJSON (gh has
  no `--slurp`; the parse is split into a pure `parseNdjson` for testing).

- presubmit: treat `startup_failure` as a failure. It was absent from
  `FAIL_CONCLUSIONS`, so a workflow that could not start counted as an executed
  run that added no failed name — an `all_pass` on a commit whose CI never ran.

- presubmit: `waiting` and `requested` are active check-run statuses; add them
  to the pending set so a commit whose only check is waiting is not mislabeled
  `no_checks`.

- pr-context: `extractCodeRefs` rendered path tokens from an untrusted comment
  body into the trusted "read each at the reviewed commit" directive. A blocker
  citing `../../../../etc/passwd.sh` or `/root/.ssh/id_rsa.key` entered the read
  list. Drop any absolute, `~`, or `..`-segment path; a real in-repo reference
  is repository-relative.

- test-efficacy: raise the probe's `spawnSync` maxBuffer to 64 MiB (the ceiling
  the gh wrapper already uses). Vitest's JSON reporter on a large suite exceeds
  the 1 MiB default, returns ENOBUFS, and turns every probe `inconclusive`.

* fix(review): comma-clause negation, RegExp flag preservation, stale comment

Third self-review round. No blockers; these are the substantive suggestions.

- pr-context: the negation stop-set gained clause separators last round but not
  the comma, so "No other concerns, but auth is a blocker" let the negation
  reach across the comma and suppress a real blocker — a false negative, the
  costly direction, flagged independently by two reviewers. Adding `,,、` leaves
  recall 2/2 and false positives 6/36 on the 38-comment corpus, and still negates
  "No blockers found, ship it".

- pr-context: `carriesBlockerSignal` rebuilt each pattern with `new RegExp(re.
  source, 'g')`, dropping any flags the pattern carried. Harmless today (no
  pattern has flags) but a latent trap the moment one gains `i`/`u`. Preserve
  the pattern's flags and dedupe `g`.

- test-efficacy: the workspace-glob comment still described the old two-pass
  filter as a present defect; the code is single-pass ordered evaluation. Fixed
  the comment to match.

* fix(review): don't revert data fixtures; guard dirty worktree; CJK non-blocker

Fourth review round (Codex $qreview + qwen). No blockers survived verification;
these are the confirmed correctness issues.

- test-efficacy: `planTestEfficacy` reverted every `kind: source` file, but
  `classifyPath` labels non-executable data under a src tree `source` too —
  JSON fixtures, `.md` bodies, snapshots. This PR ships one such fixture that
  `pr-context.test.ts` loads; reverting it deleted the file and made that probe
  inconclusive because of the probe itself. Revert now gates on an
  executable-source extension.

- test-efficacy: refuse to run when the `--worktree` has uncommitted changes to
  a revert-set file. Safe on the pipeline's ephemeral worktree, but this is a
  public command and the checkout-over-revert would discard a user's staged or
  unstaged edits with no undo.

- pr-context: `非阻塞` / `并非阻塞` is the Chinese "non-blocking" — the CJK twin
  of the `non-blocking` lookbehind. Without a guard, "非阻塞问题" promoted and
  consumed the mandatory-review budget. Same class as the bilingual-negation fix
  two rounds ago; a guard was written for one language and not the other.

- DESIGN.md: sync the pattern list (bare `blocking`, not the noun forms) and the
  Referenced-code claim (only when the blocker names a file) to the code.

* fix(review): make the dirty-worktree guard fail closed

Fifth review round. One Critical and a vacuous test, both real.

- test-efficacy: the dirty-worktree guard added last round called `spawnSync`
  directly and read `(r.stdout ?? '')`, so a spawn failure produced an empty
  string, read as "clean", and let the probe proceed — the fail-OPEN outcome in
  a guard whose whole purpose is to prevent data loss. Route it through a
  `gitOut` helper that throws on `r.error`/non-zero, and apply the same
  `r.error` check to `existsAtRev`. Verified: a dirty worktree now refuses to
  run and the uncommitted change survives.

- pr-context.test: the comma-negation test's second assertion
  (`No blockers found, ship it` -> false) was vacuous — the plural `blockers`
  matches no pattern, so it proved nothing about the negation window. Replaced
  with `This is not a blocker`, which actually exercises it.

- test-efficacy.test: add the source-only case (`probes: []` with a non-empty
  revert set), the mirror of the existing test-only case.

* fix(review): NDJSON per-line tolerance, dedupe failed checks, GFM nesting

Sixth review round — three small confirmed issues, two of them my own from
earlier rounds.

- gh: `parseNdjson` threw the whole page away if any single line failed to
  parse. `gh` can print an update/deprecation notice to stdout, so parse
  line-by-line and skip a non-JSON line instead of losing the records already
  read.

- presubmit: `failedCheckNames` used `.push()` and so listed a matrix job once
  per failing platform ("Test, Test, Test"), while the `skippedCheckNames` I
  added dedupes via a Set. Dedupe `failedCheckNames` too.

- SKILL.md: a nested `**bold**` inside a `**bold**` span (introduced when I
  qualified the Referenced-code claim two rounds ago) breaks GFM — the inner
  `**` closes the outer span, mis-rendering an agent-facing instruction. Drop
  the inner emphasis.

* fix(review): redesign blocker negation; fail-closed parse; fixture-dir revert

Seventh review round (Codex). Six confirmed issues, four of them regressions
from my own earlier fixes — a sign the patch-on-patch approach to natural
language and to the probe's file selection had to be replaced, not extended.

- pr-context: replace the pile of per-pattern negation lookbehinds with one
  negation-window model. Each lookbehind fix had opened a hole in the other
  direction: `(?<!非)` suppressed `除非` ("unless", a real blocking condition);
  the adjacency-only guard missed `并非一个阻塞项`; the comma I added to the
  stop-set broke the coordinated list "No blocking, must-fix, or critical". The
  window now scans a negation word within ~40 clause chars, RESETS at an
  adversative (`but`/`但`) but not a bare comma, and breaks at `;`/`:`. Verified
  on an 11-case matrix and the 38-comment corpus: recall 2/2, false positives
  5/36 (down from 6). Patterns are now bare, negation is one mechanism.

- gh: `parseNdjson` is strict by default and `ghApiAllNested` uses strict. Last
  round I made it lenient to tolerate a `gh` update notice, but silently
  dropping a malformed check-runs line could hide a *failing* run — the
  fail-open the pagination fix closed. Leniency is now an explicit opt-in.

- test-efficacy: the revert set excludes fixture DIRECTORIES, not non-code
  extensions. The extension whitelist also dropped runtime-loaded sources a
  test gates — an executable `SKILL.md`, a settings-schema JSON — so a
  skill-only change produced no probe. Directory is the right discriminator.

- test-efficacy: the dirty-worktree guard adds `--ignored`, so a gitignored
  revert-set path recreated locally is not read as clean and overwritten.

* fix(review): ReDoS, un-replied blocker promotion, status pagination, path guard

Eighth review round (Codex). Several confirmed defects, and one I got wrong last
round.

- pr-context: **ReDoS in `CODE_REF_RE`** — I rejected this as a hallucination
  after testing the wrong input shape. Codex's exact shape (`"(blocker)\n" +
  "a".repeat(n)`) reproduces: the two overlapping greedy quantifiers
  `[\w./@-]*[\w-]+\.` backtrack catastrophically when `\.ext` fails, ~7s at 80k
  chars on an untrusted comment body. Replaced with a single bounded class
  `[\w./@-]{0,200}[\w-]\.` — 0 ms at 80k, same matches.

- pr-context: **an un-replied blocker root was never promoted.** Only *replied*
  roots ran through `carriesBlockerSignal`; a fresh `[Critical]` with no reply
  went straight into "Open inline comments" as a 240-char snippet — the exact
  read-window failure this change exists to close, left open for the un-replied
  half. Open blocker roots now join the re-check section, rendered first and in
  full.

- pr-context: the negation window resets at a space-surrounded hyphen (` - ` /
  ` -- `), an informal clause separator, without touching `must-fix` /
  `non-blocking`.

- presubmit: paginate the legacy combined-status endpoint (same first-page-only
  gap as check-runs — a failing status on page 2 was invisible).

- test-efficacy: reject a revert path that escapes the worktree (the report JSON
  is untrusted and these become git pathspecs / fs targets), and exit non-zero
  on a restore failure so a caller cannot mistake a base-code tree for a clean
  run.

* fix(review): don't let the efficacy probe delete through a PR-controlled symlink

A reviewer reproduced a P0. The efficacy probe reverts the PR's source to base
in the shared worktree and restores it afterward, deleting files with
`rmSync(join(worktree, p), { force: true })`. `rmSync` follows symlinks in the
path prefix, and the revert set is PR-controlled, so:

  1. base has a real `dir/victim`;
  2. PR head replaces `dir` with a symlink to an outside directory and deletes
     `dir/victim`;
  3. the probe restores HEAD (the `dir` symlink), then deletes the `dir/victim`
     path — which now resolves through the link and removes the OUTSIDE file.

The lexical `escapes the worktree` guard added last round cannot catch this:
`dir/victim` is lexically inside the tree; the escape is a runtime symlink
traversal. Confirmed by driving the real handler — the outside file was deleted.

Both delete sites (reverting an added file, restoring a deleted one) now go
through `safeRmWithin`, which walks every path component from the worktree root
and refuses when an ANCESTOR is a symlink. The final component being a symlink is
still fine — that unlinks the link itself, which is what reverting an added
symlink should do. A refusal fails closed: it sets the restore-failure disclosure
and the non-zero exit, so the tree is never silently left mutated.

Verified: the P0 repro now leaves the outside file intact and exits non-zero; a
legit no-symlink PR still restores cleanly (exit 0, worktree back at HEAD).

The deeper fix the reviewer suggested — run the probe in a disposable isolated
worktree — also addresses the concurrent-read Critical and is tracked as a
follow-up; this closes the file-deletion vector now.
2026-07-13 15:45:48 +00:00
..
src fix(review): stop dropping live blockers, and probe whether new tests actually gate new code (#6790) 2026-07-13 15:45:48 +00:00
index.ts fix(cli): add bootstrap fast paths (#6188) 2026-07-02 22:28:11 +00:00
package.json feat(serve): persist dynamic workspace registrations (#6716) 2026-07-11 16:49:40 +00:00
test-setup.ts feat(serve): persist dynamic workspace registrations (#6716) 2026-07-11 16:49:40 +00:00
tsconfig.json feat(channels): add WeCom intelligent robot channel (#6436) 2026-07-07 15:24:19 +00:00
vitest.config.ts feat(cli): Add runtime daemon channel control (#6741) 2026-07-13 02:53:27 +00:00