* test(web-shell): add a mermaid diagram visual scenario
Add a `mermaid diagram` scenario to the visuals suite so the preview
covers the Mermaid rendering surface — an assistant message with a
mermaid fenced flowchart. It renders the real MermaidBlock (async
mermaid import, injected <svg>) in light and dark, waiting on the
rendered SVG so the capture is never the "rendering…" placeholder.
This is the surface the diagram zoom/pan work (#6881) enriches, so once
the before/after preview lands it gives that PR a real before/after
target instead of an unrelated canned screenshot.
* test(web-shell): add a split-view (+ maximize) visual scenario
Add a `split view` scenario: enter the two-pane split via the `?split=a,b`
deep link, then maximize one pane (#6951). Captures the tiled state (both
panes, with the maximize controls) and the maximized state (one pane
filling, restore control) in light and dark, driving the real SplitView
against the mock daemon serving two sessions.
* test(web-shell): add a sidebar attention-badge visual scenario
Add a `sidebar attention` scenario: four sessions in distinct states —
waiting-on-permission, waiting-on-user-question, running, idle — so the
sidebar renders #6956's "Waiting for approval" / "User input needed"
attention pills. Renders in light and dark; asserts on session names
(present with or without the pills) so the frame is the same shape on
main and the PR, letting the before/after preview surface the pills.
* test(web-shell): derive the split view's second session from the scenario list
Addresses a review suggestion: the split view test hardcoded the
'previous-session' id, which only worked because it is in
createWebShellDaemonScenario's default sessions list. Derive the second
pane's session from the scenario's own list instead (and throw a clear
error if absent), so a future rename/removal of that default surfaces as
a self-explaining failure rather than a confusing SSE connection timeout.
* test(web-shell): tidy split copy and mermaid width in visual scenarios
Address review nits on the visual scenarios:
- Split scenario: the mock replays the same events into both panes, so
"Here is the first pane of the split." read wrong in the second pane.
Use pane-neutral copy ("Here are the two sessions, side by side.").
- Mermaid scenario: the flowchart's rightmost node clipped at the code-block
edge at the 1280px capture viewport. Shorten the node labels (same nodes and
flow) so the whole diagram fits with margin.
Re-ran both scenarios (light + dark) locally: 4/4 pass, and confirmed in the
captures that the diagram no longer clips and the neutral copy reads correctly
in both panes.
* test(web-shell): capture split-view restore and assert all sidebar sessions
Address review nits on the visual scenarios:
- Split view: after maximize, click "Restore pane" and capture the restored
tiled layout, asserting the maximize control returns on both panes — so a
regression in the restore path is caught, not just the tiled and maximized
states.
- Sidebar attention: assert all four session names render (not just the two
waiting ones). The running session is also the loaded one, so its name also
shows in the main view — scope the running/idle checks to the sidebar
landmark so the match stays unambiguous.
---------
Co-authored-by: wenshao <wenshao@example.com>
* perf(review): scope Agent 7's build/test to the workspaces the diff changed
Agent 7's brief runs `npm run build` then `npm test` with a 120s deadline. On this
repo a cold full build is 125s, so the mandated command cannot finish inside the
mandated deadline — measured across the harness's own transcripts, 71 `npm run
build` timeouts, each verifying nothing before the model spent more turns ruling
the timeout environmental and improvising a narrower command.
`qwen review build-test` replaces it. It installs if needed, then builds only the
workspaces the diff touches (from the plan's files[] and the root package.json
workspaces) plus their dependents and dependencies, deps-first — for a leaf PR, a
handful of packages instead of all of them. When a compile fails on `TS2307:
Cannot find module '@scope/pkg'` naming a workspace, it adds that package and
retries, so a tsconfig `paths` edge into another package's sources (which
package.json never declares) self-corrects rather than being modelled. A command
that runs out of time is reported as infrastructure, never as a Critical against
the PR; a build failure in a file the diff did not touch is reported as
pre-existing.
Agent 7's brief (in lib/agent-briefs.ts) now leads with build-test and falls back
to the maven/gradle/cargo/go/pytest precedence only when it reports
`toolchain: unsupported`, so non-npm repos are unchanged. buildRoleBrief injects
the concrete command with absolute --plan/--worktree paths, beside the
test-efficacy probe it already injects.
The install moves out of Step 1 (nothing before Agent 7 needs node_modules, and
running it there blocked the whole fan-out) into build-test, with
QWEN_SKIP_PREPARE=1 so `npm ci` does not trigger this repo's prepare hook — which
builds and bundles every workspace, ~190s, wasted before a scoped build. npm ci
drops from ~161s to ~27s. And todo_write is banned during a review: this document
is the plan, and each call is a whole model turn (measured at 179-377s per run of
restating steps already written down).
`isWorkspaceMember` moves from test-efficacy.ts to the shared lib/workspaces.ts
(re-exported to keep its callers), since build-test needs the same glob walk.
* test(review): add build-test to the expected subcommand roster
* fix(review): guard the build-test prompt and treat every timeout as infrastructure
Addresses the /review findings on this PR:
- agent-prompt.ts: the build-test command block is now guarded on `pr !== undefined`,
matching the adjacent test-efficacy block. Without it, `report.prNumber` being
absent resolved `--out` to `qwen-review-pr-undefined-build-test.json` — a report the
agent writes and downstream never finds.
- build-test.ts: a timed-out `npm ci` now aborts instead of building against the
partial `node_modules` a timeout leaves behind (distinct from a `prepare` failure,
which leaves a complete tree). A timed-out test command now produces an
infrastructure note rather than "correlate this failure with the diff — a failure is
a Critical", which contradicted the brief. The handler's triple `argv` cast is
consolidated.
Tests: absolute-path + no-`undefined` assertions for the build-test command block, and
install-timeout / test-timeout infrastructure-note coverage.
* fix(review): survive the tool timeout, partial installs, local mode, and Windows
Addresses the review on this PR:
- **Tool timeout (High).** build-test runs install + builds + tests in one process;
the agent's default 120s `run_shell_command` timeout would kill it — the failure
this PR fixes, one level up. The welded command block now tells the agent to
invoke it with `timeout: 600000` (the shell tool's max).
- **Partial `node_modules` (Medium).** The install gate is now npm's completeness
marker (`node_modules/.package-lock.json`), not bare directory existence, so a
partial tree left by a timeout (here or from the outer kill) triggers a reinstall
instead of a build against half a tree. A timed-out install also removes the
partial tree before aborting.
- **Local reviews (Medium).** Agent 7 runs on local/file reviews too, which have no
worktree or PR number. The build-test block now emits there as well, scoped to the
project root, so the brief's "run build-test, below" is no longer a mandate with no
command.
- **Windows merge queue (Medium).** The real-spawn fixtures used POSIX
`touch`/`test -f`/`sleep`, which the Windows `test_windows` job (cmd.exe) fails on;
rewritten with portable `node -e`, plus explicit per-test timeouts.
Smaller points: an unmodeled workspace glob (`**`, inner `*`, `foo-*`) now returns
`toolchain: "unsupported"` instead of a false "nothing to build"; a widened-away
command is dropped from `timedOut` as well as `build[]`; the `ok` docstring is
corrected; timeout detection prefers spawnSync's authoritative `ETIMEDOUT`.
* fix(review): abort widening on a build timeout, and close the round-2 review gaps
Addresses the second review pass:
- **Widening loop ignored a timeout (Critical).** A build killed at the deadline
leaves partial output that can contain a `Cannot find module` line; the loop read
that as a too-small build set and retried under another full deadline, up to the
attempt cap — 4× the wall clock for what is infrastructure, not a graph gap. A
timed-out build now aborts at once, the way the install path already does.
- **Widening scanned trimmed output.** `unresolvedWorkspaceDeps` reads the per-command
output, which was head+tail only; a `Cannot find module` in the omitted middle
(a long tsc log) ended widening early and surfaced a real gap as a false build
failure. `trimOutput` now rescues module-resolution error lines from the middle, so
the report stays bounded and the widening signal survives.
- **Unquoted `--workspace`.** A workspace dir with a space or shell metacharacter would
split under `shell: true`; the build and test commands now quote it.
- **Handler had no error boundary.** A missing/invalid plan threw a raw stack trace as
the whole of Agent 7's result; the handler now prints the descriptive message and
exits non-zero.
Coverage the review asked for: the widening attempt-cap exhaustion, a build timeout
mid-widening, negated-workspace exclusion from the build set, the `changedFilesFrom`
error path, and `readWorkspaceGlobs`' object form.
* test(review): drive build-test through the exec seam, not real npm
The new build/test scoping tests spawned real `npm run` processes. Under the full
suite's parallelism that hung CI (each npm start is slow, and a `node -e`
setTimeout fixture leaked past the process kill), and the fixtures were POSIX-only.
These tests are about which packages get built, in what order, and how a result is
classified — not about npm's own workspace resolution. Driving them through the
injectable `exec` seam makes them deterministic, instant, leak-free and
platform-independent. No production code changes; the suite drops from a hang to
~6s.
* fix(review): support single-package and non-npm repos in build-test
Addresses the follow-up review's findings.
- **Single-package npm repos (no `workspaces`) had no build/test path (regression).**
build-test now treats a workspace-less `package.json` with a build/test script as a
single root package: it installs, builds `npm run build` and tests `npm test` (no
`--workspace`), keeping the deadline and timeout-as-data for the most common repo
shape. Only when the root has no build/test script does it report `unsupported` —
and the brief's fallback now installs dependencies first, since build-test's own
install runs only on the npm path.
- **yarn/bun workspace repos got a false "install failed".** `workspaces` is also
yarn/bun syntax, but they write no `package-lock.json`, so the completeness marker
was never present and `npm ci` fail-fasted over a usable tree. Install now runs only
when a root `package-lock.json` exists; a non-npm tree that is already present is
trusted (the build is the authoritative signal).
- **Per-command deadline lowered 600s → 300s**, kept strictly below the 600 000 ms tool
timeout the brief welds, so a single hung command's own deadline fires — and the
report lands — before the outer shell kill would discard it. (A giant PR whose
commands sum past the tool ceiling remains an acknowledged follow-up; a dynamic
cumulative budget is the full fix.)
Smaller points: the install-timeout `rmSync` is wrapped (best-effort, `maxRetries`) so
a race with orphaned grandchildren can't replace the report; the local-mode `.`
worktree fallback is gated on `pr === undefined` so a PR-mode report never builds the
user's checkout (help text corrected); the now-unreachable widen-path `timedOut` filter
is removed; and `changedFilesFrom` rejects a non-object plan with a descriptive error.
* fix(review): close the generic-repo false-green and false-Critical gaps
The verification review found three ways the generic (non-this-repo) surface
re-opened the two failure modes this command is built to prevent. All three now
hand off or self-correct instead.
- **F1 (false green).** A changed dir the workspace globs map to a non-package — a
nested package listed before a `*` that also claims its parent segment, or a loose
file under a `packages/*` base — was dropped from the build set silently: zero
commands, `ok: true`, "Everything passed". Any affected dir not in the package map
now trips the `unsupported` handoff (naming the dir), never a bare green.
- **F2 (false Critical).** A review worktree is cold. A yarn/bun/pnpm repo (same
`workspaces` field, no `package-lock.json`) got no install on any path — build-test's
install is npm-gated and the brief's fallback never fired — so the build failed with
`Cannot find module` inside the PR's own files, steered toward a Critical. A non-npm
repo with no installed tree now hands off, naming the tool (`yarn install
--frozen-lockfile`, etc.) to install with first.
- **F3 (mis-order terminal-fail).** When both the needer and an undeclared-needed
package are changed and the alphabet orders the needer first, the compiler named an
in-set package, `missing` came out empty, and the run terminal-failed though the
corrected order builds green. The widening filter is now `!built.has(dir)` rather
than `!set.includes(dir)`, so an in-set-but-unbuilt package re-seeds into
`alsoBuild` (which sorts first) and fixes the order; the attempt cap still bounds it.
Tests for all three, matching the review's runnable repros. The non-blocking notes
(cumulative-budget overrun, incremental report write, array-args spawn) remain for the
dynamic-budget follow-up.
* test(review): tighten the widening-cap assertions
Per review: assert exactly three widenings (`toBe(3)`, not `>= 3`) so an
over-widening regression is caught, and assert `rep.test` is empty — the
exhaustion branch returns before the test loop, so a refactor that reordered the
test loop above that return would otherwise pass undetected.
* fix(test): widen model-response timeouts in SDK E2E tests for CI stability (#6979)
The 'should handle control responses when stdin closes before replies'
test armed 30 s boundedPromise timeouts for all four phases of a
two-turn model interaction. On shared Linux CI runners a single model
round-trip routinely exceeds 30 s, exhausting every retry.
The permission-control suite used a flat TEST_TIMEOUT = 30 000 ms
across 23 real-API test cases, and two tests hardcoded 40 000 ms
timeouts instead of the shared constant.
- Raise all four boundedPromise timeouts to 60 s in the stdin-close
test so each phase can absorb a slow model round-trip.
- Make TEST_TIMEOUT CI-aware (60 s on CI, 30 s locally) and route the
two hardcoded 40 s values through it for consistency.
* fix(test): use CI-conditional phase timeouts in abort-and-lifecycle E2E (#6985)
Apply the same CI-conditional pattern (60s on CI, 30s locally) used by
permission-control.test.ts to the stdin-close test's four boundedPromise
calls. Local developers no longer wait the full CI-grade 60s per phase.
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Update MAINLINE_CODER_MODEL from qwen3.5-plus to qwen3.7-max and
refresh the Qwen OAuth coder-model description to reflect the new
model generation.
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
* feat(cron): add deterministic test seam for cron-interactive E2E
Add forceFireJob(id) method and QWEN_CODE_TEST_CRON_FAST env var to
CronScheduler. When enabled, newly created session-only jobs auto-fire
after 5s (configurable via QWEN_CODE_TEST_CRON_DELAY_MS) instead of
waiting up to 60s for the wall-clock minute boundary.
This removes the timing-flakiness from cron-interactive.test.ts
without changing any production behavior (seam is env-gated and
inactive by default).
Refs #6982
* test(cron): use QWEN_CODE_TEST_CRON_FAST seam, reduce timeouts to 30s
Enable the CronScheduler test seam in cron-interactive tests via
QWEN_CODE_TEST_CRON_FAST=1 in makeEnv(). This makes newly created
cron jobs auto-fire after 5s instead of waiting for the wall-clock
minute boundary.
Reduce all waitForScreen timeouts from 90s to 30s since the fire is
now deterministic (~5s after job creation + model round-trip).
Refs #6982
cron-interactive.test.ts is inherently timing-flaky: it relies on
wall-clock cron fire (*/1 at minute boundary) + real model latency,
with a 90s waitForScreen window. On slower macOS runners this
occasionally exceeds the timeout, turning push CI red and triggering
wasteful autofix attempts (~50min).
Changes:
- Exclude cron-interactive from push-triggered Linux and macOS jobs
via vitest --exclude
- Add new cron-interactive-nightly job: runs only on schedule/
workflow_dispatch, with continue-on-error so flakes are visible
but do not fail the workflow
- Mirrors the existing web-shell-browser-regression pattern
Coverage is preserved: cron regressions are still caught by nightly
runs within ~24h. The root fix (injectable clock seam in
CronScheduler) is tracked separately in #6487/#6982.
Closes#6982
* feat(review): prove Step 4 (verify) and Step 5 (reverse audit) actually ran
`check-coverage` proves Step 3 was done, from the harness's own transcripts. But it
runs at Step 3D — before verify and reverse audit exist — so its roster never reaches
them, and a run could skip Step 4 or Step 5 wholesale, or launch agents that never
opened their brief, and nothing would see it. That is the same silent-omission failure
the coverage gate was built for, one pipeline stage later.
Their count is not in the plan (verify shards on the finding count; the reverse audit
loops until it goes dry), so there is no exact roster to check. What there is is a
floor, and `compose-review` is the place to check it: it runs only at high effort —
the only effort at which verify and reverse audit run at all — and it already
recomputes coverage from the transcripts on the way to the verdict.
`verificationGaps` asks two questions of the same records:
- **Reverse audit** — required on every high-effort review, because it is the pass
that looks for what Step 3 missed, and a verdict that never ran it cannot certify
the diff complete, least of all a clean one. At least one auditor must have run and
opened its brief (3A records it under `reverse-audit`, 3B under
`reverse-audit--chunk-N`).
- **Verify** — required once the review has findings, because an unverified finding
must not become a public blocker. Keyed to inline findings: a review that confirmed
nothing has nothing to verify, and deterministic `[build]`/`[test]` findings are
pre-confirmed and skip verification by design.
A gap is named in `unreviewedDimensions` and caps the verdict exactly like a dimension
nobody reviewed — an Approve drops to Comment, the gap is disclosed in the body, the
findings still post. The highest-value catch is a clean, zero-finding review that
never ran its reverse audit and would otherwise have approved. Nothing is passed in
and nothing can turn it off: the proof is the intersection of the prompt the CLI
recorded building and the harness's transcript of an agent that ran it and read the
brief — two artifacts, neither authored by the orchestrator.
Tests: verificationGaps over real transcript/record fixtures (reverse audit ran /
skipped / built-but-brief-unread / 3B per-chunk key; verify required-when-findings /
not-when-clean / brief-unread), and compose-review integration (the cap reaches the
verdict and the body).
* fix(review): tighten the Step 4/5 gate from its own review
Six findings from the /review skill's pass over this PR, all addressed.
The verify floor keyed on inline findings alone, so a non-deterministic Critical
that could not be anchored — a body Critical — posted under REQUEST_CHANGES without
ever demanding a verifier. It now counts body Criticals too, minus the deterministic
`[build]`/`[test]` ones, which are pre-confirmed and skip verification by design (and
carry their source tag, so they are recognisable). A review whose only finding is a
real unanchorable Critical is now told it was not verified; one whose only finding is
a build failure is not — that disclosure would be false.
The brief-open check matched the brief path as a bare substring, the trap
`parseTranscript` already learned to avoid for the diff path: `…/x.brief.md.bak`
would have counted as `…/x.brief.md`. Both this file's brief checks — the Step 4/5
one and the roster's — now match the whole quoted JSON value.
The per-chunk reverse-audit key was recognised by a regex hardcoding the
`--chunk-<n>` shape; it now keys off the role name and the universal `--` separator,
so a change to how the suffix is spelled does not silently drop every per-chunk key
and cap a correct review.
`verificationGaps` now runs in its own try, so a read failure there reports itself
rather than wearing the coverage block's "cannot show the diff was read" message
over a coverage result that succeeded a line above it.
Tests: the fixture helper records prompts under the percent-encoded key, matching
production; a `launch: false` verify case covers the transcript-matching half of the
floor that the `opensBrief: false` case does not; and two compose-review cases pin
the body-Critical verify rule (a non-deterministic one demands a verifier, a
`[build]` one does not).
* docs(review): correct the verify-floor comment and cover the .bak brief arm
Two non-blocking notes from the verification review.
The `verificationGaps` doc comment still said the verify floor was "keyed to INLINE
findings" — one commit stale. It is `opts.postsFindings`, decided by the caller,
which now counts non-deterministic body Criticals and excludes deterministic
`[build]`/`[test]` ones. The comment now says that.
The `.bak` tightening of the brief-open check had a regression test for the Step 4/5
arm but not the Step 3 roster arm they share. Added one: a roster agent that opened
`<brief>.bak` is not credited with opening the brief.
The 'should handle control responses when stdin closes before replies'
test armed 15 s timeouts for canUseToolCalled and inputStreamDone —
both measuring the second turn, which requires a full model round-trip
(API request + model thinking + tool_use response). On shared Linux CI
runners the second-turn latency routinely exceeds 15 s, failing all
three retry attempts.
Raise both to 30 s, matching the existing firstResult and secondResult
budgets, so every phase in the two-turn test carries the same headroom.
Co-authored-by: Qwen Autofix <autofix@qwen-code.dev>
* fix(web-shell): show workspace chip tooltip on narrow composer
The composer's workspace chip surfaced its full cwd only through a native
`title` attribute, unlike the sibling git-branch and model chips which use a
styled Radix tooltip. On a narrow (split-screen / mobile) composer the chip
ellipsizes or collapses to an icon, so the workspace is discoverable only on
hover — and a native `title` is inconsistent and never fires on touch.
Give WorkspaceIndicator the same Radix tooltip as GitBranchIndicator (with the
full cwd as content), completing the documented "mirrors GitBranchIndicator"
intent. Its visually-hidden tooltip mirror also exposes the cwd to screen
readers, which the native `title` did not do reliably.
* test(web-shell): assert the workspace tooltip renders on hover
Address review feedback: the WorkspaceIndicator tests checked the
`data-web-shell-workspace-title` hook but never opened the tooltip, so a
regression rendering the short name (or nothing) in the Radix `TooltipContent`
would have gone unnoticed. Open the tooltip via a `pointermove` (jsdom has no
`PointerEvent`; Radix opens on mouse move after `delayDuration`) and assert the
portalled `[role="tooltip"]` shows the full cwd — and, in compact mode, assert
the `workspaceChipCompact` icon-only class is actually applied.
* test(web-shell): guard the compact chip before asserting on it
Move the `if (!chip)` null guard above the assertions in the compact-mode test
so a failure to render surfaces the descriptive "workspace chip was not
rendered" error instead of an opaque `expect(undefined)` throw from the
optional-chained access. Matches the first test in the file.
---------
Co-authored-by: wenshao <wenshao@example.com>
The visual-preview capture workflow triggered on
packages/sdk-typescript/src/**, but the previews render against a mock
daemon — the SDK's transport/client layer is stubbed at the network
boundary and its changes can't move a pixel in the rendered scenarios.
So #6911 (a pure-backend PR that only added a DaemonClient data-layer
method) still got the five canned screenshots re-posted as noise.
Drop the SDK trigger. The web-shell client imports no runtime code from
the SDK root and only type-imports DaemonClient, so no real UI coverage
is lost: genuine web-shell UI PRs (#6881, #6951) still trigger via
packages/web-shell/client/**. Confirmed by simulating the path predicate
against each PR's changed-file list.
Co-authored-by: wenshao <wenshao@example.com>
* feat(web-shell): maximize a single split pane
Add a per-pane maximize/restore toggle to the split view. Clicking it makes
one pane fill the whole split and hides the others; the hidden panes stay
mounted so their sessions keep streaming (a purely visual solo). Restore via
the header button or Escape — Escape defers to the composer, the add-session
picker, and open dialogs so it never steals their key.
The toggle only appears with 2+ panes, adding a session exits maximize to
reveal the new pane, and the maximize is dropped whenever its pane leaves the
set or the split shrinks to a single pane.
* refactor(web-shell): use lucide icons for the maximize toggle; cover switch + picker-Escape
Address review on #6951:
- Swap the hand-written Maximize2/Minimize2 SVG paths for the named lucide-react
components, per the web-shell icon convention (README) and matching DialogShell.
- Add tests for moving maximize between panes (guards the toggle's switch branch)
and for Escape closing the add-session picker without un-maximizing (guards the
pickerOpen deferral).
---------
Co-authored-by: wenshao <wenshao@example.com>
* feat(review): build the Step 4 verifier and Step 5 reverse-audit prompts in code
The last change moved Step 3's agent prompts into code — the diff path, the brief,
the roster, the delivery check — because a prompt the orchestrator retypes is one
that drifts, and dogfooding proved every drift. Step 4 (verify) and Step 5 (reverse
audit) were left composing their prompts from prose. They carry the densest
methodology in the skill, and it is the methodology most costly to drop:
- the verifier's one-way, quote-the-contradiction bar on rejecting a Critical, and
its documented-intent gate — the exact rule a run skipped when it auto-posted a
false "this PR now leaks AWS/GitHub tokens" Critical over a rationale three lines
up in the diff;
- the reverse auditor's gaps-only focus and substantive receipt.
Both are now `qwen review agent-prompt --role verify` / `--role reverse-audit` —
built in code, written to a brief file the agent reads, so the launch prompt the
orchestrator carries is short and the method cannot be paraphrased away. The
verifier is a new brief kind (`output: 'verdicts'`): it gets the Exclusion Criteria
but not the finding format, because it rules on findings rather than filing them. A
Step 3B reverse auditor takes `--chunk <id>` so it reads one chunk's range, not the
whole 5 800-line diff — the range that made it the most context-starved agent in the
pipeline.
The orchestrator still supplies the one input that changes per launch — the shard's
findings for the verifier, the cumulative finding list for the auditor — above the
verbatim brief, exactly as a dimension agent gets its one-line change summary.
NOT in this change, and called out so it is not mistaken for done: these agents run
after Step 3D, so the coverage gate's roster does not reach them. Whether the
verifier and auditor actually ran and read their briefs is not yet checked from the
transcripts the way Step 3's agents are. That check is the next step.
* fix(review): scope a per-chunk reverse auditor's brief to its one chunk
Dogfooding this PR, the /review skill's own reverse-audit agent found a defect
in it. A Step 3B reverse auditor is launched `--role reverse-audit --chunk N` so
it reads one chunk's range, not the whole diff — that scoping is the whole point:
a reverse auditor handed a 5 800-line diff is the most context-starved agent in
the pipeline, on exactly the PRs where the reverse audit matters most. The launch
prompt scoped correctly. The brief did not: `buildRoleBrief` gave every diff-reading
role the full `diffReadingBlock`, which emits a read for every chunk in the plan and
says "walk it chunk by chunk". The agent is told its brief is authoritative and that
nothing in the launch message replaces it — so it would read the whole diff the
`--chunk` design exists to spare it. The brief and the launch prompt disagreed on the
one thing the feature is about.
`diffReadingBlock` now takes an optional chunk id and, when given one, reads that
chunk alone — the same range the launch prompt reads — and drops the "walk it chunk
by chunk" instruction. `buildRoleBrief` threads the chunk through, exactly as an
invariant agent's brief is already scoped to its one file.
Which role may be launched per-chunk is now declared on the brief (`acceptsChunk`),
not hardcoded in the command guard as `role !== 'reverse-audit'`. A new per-chunk
role is a data change in agent-briefs, and the guard reads the same field the brief
builder does. Tests added for the fix and the coverage the review flagged: the
scoped brief reads one chunk not all; the handler accepts the one legal role+chunk
combo and keys its record `reverse-audit--chunk-N`; a non-existent chunk id is
rejected by name rather than emitting an unusable read.
* fix(review): finish the acceptsChunk story and de-duplicate the diff-window math
A second reverse-audit pass — the /review skill run on the previous commit — found
four gaps in it. All four are addressed here.
The guard was made data-driven (`!BRIEFS[role]?.acceptsChunk`) but its error message
still said "only for reverse-audit". If a second role ever sets `acceptsChunk`, the
guard would allow it while the message denied it. The message now names the set it
read from the briefs, so it cannot drift from what the guard enforces.
The record key derived from `--file` with no guard on it, while `--chunk` was guarded.
`--role reverse-audit --chunk 14 --file foo.ts` was accepted and keyed
`reverse-audit--foo.ts` — a file the agent never reads, colliding with and masking a
real file-keyed record. `--file` is the invariant agent's one scoping input; it is now
rejected on any role that is not an invariant agent, and on `--whole-diff`, closing the
asymmetry with the `--chunk` guard.
The 1-based-line-range → `{offset, limit}` arithmetic (`startLine - 1`,
`endLine - startLine + 1`) was written out at five sites. An off-by-one fix, or a
change in how `read_file` windows, would have had to land in all five. It is now one
`diffWindow(startLine, endLine)` helper the five call.
Tests: the message names the derived set; `--file` is rejected on a non-invariant role
and on `--whole-diff`; and the verify role — the one new role whose full handler path
was untested — is driven end-to-end through the handler, keyed `verify`, with the
verdict branch of its brief (Exclusion Criteria, no finding format).
* feat(web-shell): add zoom and pan controls to Mermaid diagrams
* fix(web-shell): bind Mermaid drag events to window and address review nits
- Move mousemove/mouseup listeners from wrapper div to window via
useEffect, preventing drag cancellation when cursor exits the
container during fast pans (Critical from review)
- Clamp Y-axis offset to ±1500px to prevent dragging into the
overflow-y: hidden clipped region; X-axis remains unclamped
since overflow-x: auto provides native horizontal scrolling
- Remove unused wrapperRef (dead code)
- Merge duplicate handleZoomReset/handleDoubleClick into shared
resetZoomAndPan callback
- Add missing title attribute to zoom reset button for tooltip
accessibility parity with zoom-in/zoom-out buttons
* fix(web-shell): address review round 2 — drag state, max-width, code change reset
- resetZoomAndPan now clears dragRef and isDragging, preventing stuck
grab cursor when user alt-tabs or loses focus during an active drag
- Add window blur listener in drag useEffect to cancel drag on focus
loss (alt-tab, browser notification, DevTools stealing focus)
- Add flowchart.useMaxWidth: false in mermaid.initialize so the CSS
max-width: none rule actually takes effect (Mermaid v11 stamps an
inline max-width via useMaxWidth:true by default, overriding CSS)
- Reset zoom and offset when code prop changes (diagram regenerated)
via a dedicated useEffect keyed on code; theme-only toggles preserve
the user's current zoom/pan state
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
The submit.test.ts posting-gate tests pass a test-only skillArgs path,
which authorization() honours only when currentSessionId() is empty.
CI sets QWEN_CODE_SESSION_ID, so every skillArgs-based test read the
session-scoped path instead — which never existed — and the gate
refused every post.
Save and clear QWEN_CODE_SESSION_ID in beforeEach, restore in
afterEach, matching the pattern the session-id test already uses in
its own try/finally.
Co-authored-by: Qwen Autofix <autofix@qwen-code.dev>
* fix(review): prove coverage on both topologies, and check the prompt survived the trip
Three defects, all measured against the harness's own transcripts of real /review
runs against QwenLM/qwen-code PRs #6766 (Step 3B) and #6579 (Step 3A).
1. Step 3A reviews were told nobody had read them.
Coverage was attributed by one question: did an agent whose launch prompt says
`chunk N of M` make a successful tool call? No Step 3A prompt says that — there
every dimension agent walks the whole diff — so no chunk was ever attributed to
anyone. Run against a real 3A review whose twelve agents each opened the diff,
walked both chunks and filed findings, check-coverage returned
Coverage: 0/2 chunk(s) reviewed. 16 agent(s) ran; 16 did work
ERROR: 2 chunk(s) were not reviewed — 1, 2. Nobody read those lines.
in one breath. compose-review runs the same computation on the way to the
verdict, so a flawless small-PR review was capped away from Approve and the
body it would have POSTED to the pull request said nobody had read it. Step 3A
is the topology most pull requests get. The only reason it never blew up is
that check-coverage lived inside Step 3B and was never reached from 3A — two
bugs cancelling.
Coverage is now the intersection of two things the harness wrote down: the
lines each agent was pointed at (its launch prompt) and the fact that it opened
the diff (a successful tool call naming the diff file). Topology-blind. It also
no longer credits a chunk to an agent on the strength of any successful call —
a glob for test files was enough.
2. The whole-diff agents were still launched blind.
agent-prompt built the territory agents' prompts and left the other half of the
fan-out to prose. All three whole-diff agents of the 3B run — cross-file tracer,
test-coverage matrix, build & test — got a prompt naming no diff file at all.
The test-coverage matrix was told to "Read the diff chunks" and given no path to
read them from; it read the post-change source instead, which on a deletion
shows it nothing. These agents own the classes a chunk agent is structurally
blind to, and the gate could not see it: it only asked that question of agents
whose prompt said `chunk N of M`.
`agent-prompt --whole-diff` now builds their diff-reading block too.
3. The prompt the CLI built was rewritten on the way to the agent.
The 3B run invoked agent-prompt correctly for all five chunks and then
paraphrased what it printed: the delivered prompt dropped the rule against
reciting a stock sentence, dropped the half-read warning, replaced the project's
review rules with a three-sentence summary of its own, and invented an
instruction that was never in the original. Nothing could see it, because a
paraphrase keeps the diff path. So agent-prompt records what it emitted, at a
path derived from the plan that the caller is never given; check-coverage reads
it back against the launch prompt the harness recorded. Replayed against that
run's real transcripts, all five chunk agents are now named.
Verified end to end: a fresh /review of #6829 (3A) calls `agent-prompt
--whole-diff`, passes it verbatim, and Step 3D reports 2/2 chunks reviewed,
12 agents, 12 did work — a gate that path could not reach before, and could not
have passed if it had.
* fix(review): build every agent's prompt, from a roster the plan derives
Two more failures, both measured against the harness's own transcripts of real
/review runs.
1. Agent 0 was never launched, and nothing could tell.
The skill says issue fidelity runs on every PR review. Dogfooded on #6766, it
did not run — and every check passed, because every check asks a question of an
agent that RAN. An agent that does not run leaves no transcript to ask. An
omission is invisible precisely because it is an omission.
So `check-coverage` now derives a roster from the plan — which the caller does
not write — and names every required agent that never ran, with the exact
`agent-prompt` call that builds it. The plan already knew everything the roster
turns on: the topology, whether the diff deletes anything, which files were
rewritten heavily enough to need invariant agents, whether there is a worktree
to build in and a pull request to check an issue against.
`agent-prompt --role <role>` builds all of them: 0, 1a, 1b, 1c, 2, 3, 4, 5,
6a/6b/6c, 7, the test matrix, and the three invariant agents per heavy file.
The briefs move out of SKILL.md and into code, because a brief the orchestrator
retypes is a brief that drifts.
2. A 4 652-character prompt is not a thing an orchestrator will paste twelve times.
With the briefs welded into the launch prompt, the first dogfood delivered
**2 893** characters of one: it kept the head, added a preamble of its own, and
cut nineteen hundred characters out of the middle. The delivery check caught it
— and the run then read the check's exit-3, concluded "the agents clearly did
their job", skipped `compose-review`, and filed an **Approve it had written
itself**. A gate that always fails is a gate that gets talked around.
So the brief goes where the diff already goes: on disk, read by the agent that
needs it. The launch prompt drops to ~500-800 characters — it names the role,
points at the brief file, and lists the diff reads — and whether the agent
actually read its brief stops being a hope and becomes a tool call the harness
wrote down (`unreadBriefs`).
Verified end to end. A fresh /review of #6847 built all twelve role prompts, and
against the harness's transcripts: 12 of 12 delivered **byte-for-byte verbatim**,
12 of 12 **opened their brief**, 3-19 successful tool calls each — Agent 0 among
them. Step 3D: `1/1 chunks reviewed, 12/12 agents did work`, no errors.
* fix(review): the verdict is computed, not carried
`compose-review` has computed the event and the body since the C/S table stopped
being prose. The skill then told the orchestrator to "copy event/body verbatim
into the review JSON" — a transcription, into a document the model writes, of a
decision the CLI had already made. That is the exact anti-pattern `submit`'s own
header repudiates, and it left two ways for a run to author its own verdict:
- **The terminal.** Step 6's verdict was composed by the model, from prose
rules. Dogfooded, a run read the coverage check's refusal, concluded that
"the agents clearly did their job", never called `compose-review` at all, and
printed `Review complete — Approve` on a review whose gate had just refused.
- **The wire.** `submit` took `{event, body}` as fields. Nothing stopped a run
that had skipped the computation from posting the conclusion it preferred.
So `submit` composes. It takes the findings — the inline comments and the states
Step 6 established — and derives everything that follows, including how many
blockers there are: `criticalsInline` and `suggestionsInline` are counted off the
`**[Critical]**` / `**[Suggestion]**` prefixes of the comments actually attached,
not accepted as numbers beside them. (A number beside a list is a number that can
disagree with the list, and one did: the breaching run posted a body reading
"Suggestions are inline" next to an empty `comments` array and a summary claiming
`0 Suggestion inline`.) A payload carrying `event`/`body` is refused rather than
silently overruled — the caller was trying to author a verdict.
Two body checks are deleted, not weakened: a body that promises inline comments it
does not carry, and a body whose footer is preceded by a literal `\n`. Both were
checks on a string the caller built. The caller no longer builds it.
`compose-review` now prints the verdict line itself, and Step 6 prints that. There
is one place a verdict exists; skipping the command does not get you a different
one, it gets you none.
Verified: a payload with `event: APPROVE` and an unreviewed dimension is refused
at the wire; the same findings without a verdict compose to `COMMENT`. A fresh
/review of #6788 called `compose-review`, was told `Verdict: Comment`, and showed
the user "Comment — downgraded from Approve (CI failing: route)" — the presubmit
downgrade applied by code, on a run that did not post.
* fix(review): make Step 3B carryable, and stop the delivery check crying wolf
Dogfooding the Step 3B path — the one topology none of this had been run against
— found two defects, and the second is the more important of the two.
1. Eighty-seven kilobytes of chunk prompts, in one response.
The briefs moved onto disk for the dimension agents and not for the territory
agents. Measured on PR #6606 (5 511 diff lines, 17 chunks): 17 chunk launch
prompts of ~5 149 characters each — **87 546 characters** the orchestrator was
expected to paste unedited. At a twelfth of that load it had already cut
nineteen hundred characters out of a single prompt.
Chunk agents get the same split: the brief on disk, and a launch prompt that
carries only what cannot live anywhere else — `chunk N of M`, which attributes
the territory, and the `offset`/`limit`, which are the lines coverage proves
were delivered. 87 546 → 14 789 characters. And `check-coverage` now asks the
territory agents the same question it asks the others: did you open your brief?
2. The delivery check failed a correct run — all nine agents of it.
It was a substring test: the built prompt had to appear in the launch prompt,
contiguously. That is a stricter claim than the skill makes, and both of the
differences it fired on were legitimate. The orchestrator had inserted **the
one-sentence summary of the change that the skill explicitly tells it to add**,
which breaks contiguity by construction — and it had reflowed a hard-wrapped
sentence onto one line, which changes not one character of meaning.
This is the failure this skill keeps re-learning, and this time it was ours: a
gate that fires on a correct run is a gate that gets talked around, and there is
a dogfood transcript of a model doing exactly that. The rule the check enforces
is now the rule the skill states — **you may add; you may not remove, alter, or
reorder** — over whitespace-collapsed lines, in order.
Verified against the harness's transcripts of a real Step 3B review of #6766: nine
agents (five chunks, issue fidelity, cross-file tracer, test matrix, build & test),
9/9 delivered intact, 9/9 opened their brief, 6-22 successful tool calls each.
`check-coverage`: 5/5 chunks, every list empty, exit 0.
* feat(review): path-scoped rules; and move the briefs out of the skill, where they were never reaching the agents
Two changes, and the second found a hole the first would not have.
1. Rules that attach to a path, not to a dimension.
The nine dimensions are domain-blind by design — "find security bugs" is a lens,
not a syllabus — and that holds until a file's failure modes are not guessable
from reading it. A GitHub Actions workflow is the clearest case: it is YAML, so it
reads as configuration, and the reviewer who treats it as configuration misses
every one of its attack classes. Nothing in this review knew to ask whether a
`pull_request_target` job checks out the contributor's head — which is the
difference between a CI file and a remote code execution with the repository's
write token. This repo runs `qwen-autofix.yml`, which posts to pull requests.
`agent-prompt` now appends a checklist for such a file to the brief of every
code-reviewing agent **whose territory actually contains one**. Scoped, because a
rule that fires on every review is a rule that gets skimmed. `/review` runs on
other people's repositories, so the calibration matters as much as the content:
the blockers are the six that are unambiguously wrong; the two that shade into
taste (SHA-pinning, `permissions:`) are Suggestions, exempt the conventions almost
everyone keeps, and are scoped to lines the diff touches. No style rules — a
linter owns those, and the Exclusion Criteria already forbid them.
2. The briefs move out of SKILL.md — and three things turned out never to have
reached an agent at all.
The briefs have been built in code since the roster landed, and SKILL.md still
carried 38 KB of the same prose. Duplication is drift, and a 178 KB skill is
~45 000 tokens in the orchestrator's context on every review — which is itself a
cause of the failure this whole line of work has been chasing. The skill now keeps
what each agent is *for* (a table) and drops what it is *sent* (the command's copy
is the one that arrives).
Doing that surfaced what the code briefs were missing, because the deleted prose
had to go somewhere:
- **The Exclusion Criteria had never reached an agent.** The skill states them at
the end of the document and tells the orchestrator to "apply" them. The agents
do not read the document. The single largest precision control in this review
has been governing nobody, in every run, since it was written.
- **Nor had the anchor rules.** Agents were asked for a snippet and never told
what makes one resolvable: prefer added lines, a removed line cannot be anchored
at all, a bare `}` matches everywhere. `resolve-anchors` was downstream of a
snippet nobody had given the rules to produce.
- **Nor the severity calibration.** `SEVERITY`'s own comment warns that a chunk
agent owns test coverage with nothing to calibrate it and will file "zero test
coverage" as Critical — and then did not include the calibration.
All three are in the briefs now. And two degradations the orchestrator used to be
told to add by hand — and can no longer add, because it does not write these
prompts — are applied by the builder: in cross-repo lightweight mode there is no
tree, so 1b and 1c report at `Confidence: low` rather than asserting a
re-establishment is missing. A false Critical blocks a merge.
Step 3C (the medium-effort inline pass) now *loads* the briefs it needs rather
than carrying them: same text as the high-effort agents get, read when that level
actually runs instead of sitting in every review's context.
SKILL.md: 171 178 → 153 656 bytes.
* fix(review): scope an invariant agent to its own file, and let a blocker's blast radius be part of the blocker
Two corrections, both from dogfooding the paths that had never been run.
1. An invariant agent was being handed the whole chunk plan.
It owns one heavily-rewritten file. Its brief says so, and gives it that file's
own slice of the diff. Its *launch prompt* listed every chunk in the review —
on PR #6457, all twenty-one reads of a 6 149-line diff, for an agent whose job
is one file.
The wasted reading is the smaller half. Coverage is computed from the ranges in
the launch prompt, so an invariant agent was being credited with having read
**every chunk in the review**. One of them could have masked twenty missing chunk
agents. It now gets exactly its file's `diffRange`, and nothing else.
2. `permissions: write-all` on a job that runs untrusted code is not a Suggestion.
The path rule said it was. Dogfooded against a planted vulnerability, the security
agent read that and escalated anyway: "grants maximum token scope to a job that
processes untrusted contributor code, amplifying the RCE above". It was right and
the flat rule was too coarse. A broad token on a privileged job is not a separate
recommendation — it is how far the blocker reaches, and it belongs in that finding,
at Critical. On an ordinary job it stays a Suggestion.
* fix(review): the six findings this skill filed against its own pull request
The repository's own `/review` bot reviewed #6892 — this change reviewing the code
that changes it — and filed six Suggestions. Every one of them is real, and two are
fail-open holes in the gates this pull request exists to build. They are fixed here,
each with a test.
- **`submit` accepted `state: null`.** `=== undefined` is not `== null`, so the
structural check passed it; `compose`'s `?? {}` then collapsed it to an empty state
and would have posted a review whose footer named no model and whose caps came from
nowhere.
- **`wasDeliveredVerbatim` was vacuously true for an empty `built` prompt.** A
zero-byte record is what a partial write leaves behind — and `recordPrompt` swallows
its write errors by design, so this is reachable. `readRecordedPrompts` stores it as
`''`, not `undefined`, so the "no prompt was built" guard did not catch it, and the
loop's body never executed. The roster would have credited a required role to
whichever transcript it looked at first. It now fails closed.
- **A chunk read across two pages got no credit.** The check asked for a *single*
range containing the chunk, and reads of 1-200 and 201-400 are two — so it
contradicted the paging instruction the same review had just given, on exactly the
oversized chunks where paging is not optional. Ranges are coalesced first.
- **Agent 7 was handed relative paths it could not resolve.** `worktreePath` and the
plan path are repo-relative in the report, and Agent 7's working directory *is* the
worktree — so `--worktree .qwen/tmp/review-pr-6457` resolved to
`<worktree>/.qwen/tmp/review-pr-6457`, which does not exist. This was already
visible and nobody had read it: in the 29-agent dogfood run, Agent 7 spent its time
running `find … -name "*6457*fetch*"`, hunting for a plan it had been handed a path
to. Absolute now.
- **`removePromptRecord` was dead code with a comment claiming a caller it did not
have.** `cleanup.ts` sweeps the prompt directory by prefix instead. Deleted.
- **`--dry-run` omitted `cappedBy`.** The point of a dry run is to see what would be
posted; `"event": "COMMENT"` with no reason leaves the reader to guess why the
Approve went away.
* docs(review): purge the stale event/body payload examples from Step 7
The reviewer caught a real contradiction it filed as Critical: submit.ts now
refuses a payload carrying `event`/`body` (those are computed from `state` and
the attached comments), but Step 7's main-path 'Build the review JSON' examples
still showed `"event": "REQUEST_CHANGES"` / `"body"` and routed the verdict
through a copy-it-verbatim step. An orchestrator following the unchanged
instructions would have built exactly the payload submit rejects.
The correct `{commit_id, comments, state}` shape existed lower in the section (the
no-findings branch), added when submit took over composition — but the main-path
examples and the compose-review-then-transcribe bullets above them were never
reconciled. They are now: one payload shape, no verdict in it, `state` handed to
submit, and the inline counts derived from the comments rather than supplied.
Found by the repository's own /review on #6892.
* fix(review): the three findings from the third self-review
The repository's /review passed #6892 (no blockers) and filed three Suggestions.
All three are real; two are contradictions this PR itself introduced.
- **verdictLine printed a dangling colon.** When a would-be Approve was taken away
by a presubmit downgrade ALONE — no cap state, `cappedBy` empty, `downgraded`
true — the code joined the empty array and produced 'an Approve was NOT
available: — downgraded by a presubmit check', a colon over nothing. It now
collects the reasons (a cap and a downgrade are both reasons, either can be the
only one) and prints the clause only when there is a reason to. The function had
no test; it has six now, including this case.
- **Step 3D said 'six failures' and listed seven, while check-coverage reports
eight.** The count drifted as failure classes were added, and the uncoverable-chunk
class had no bullet at all. Now 'eight', with the missing bullet written.
- **`submit --review` help still advertised `event` / `body`** as payload fields,
which the same command now refuses. Updated to `commit_id / comments / state`.
Found by the repository's own /review on #6892 — the third pass, the one that
turned CHANGES_REQUESTED into no-blockers.
* fix(review): a heavy file in a Step-3A diff must not demand invariant agents
From a human review of #6892 (doudouOUC). `heavy` is decided independently of
topology (lib/heavy.ts): a ~300-line source file with ~120 changed lines clears
the rewrite-ratio branch while srcDiffLines stays under 500 — a Step 3A review.
The invariant-agent loop in requiredAgents ran in both topologies, so it added
invariant-a/b/c to the roster of a 3A review that never launches them; check-coverage
then reported them as missingRoles and exit-3'd, and compose-review capped the
verdict — an otherwise-complete small PR, falsely blocked.
Gate the loop on isTerritoryFanOut. Step 3A's dimension agents each walk the whole
diff, so one already sees both ends of a rewritten file; invariant agents are a 3B
mechanism for when the diff is carved into territories and no single agent holds
the whole file. roster.test.ts now pins the 3A-heavy case.
Also, same review: merge() in coverage.ts copied its first tuple and pushes copies,
so it no longer mutates a tuple owned by rec.diffReads (harmless today, pure now).
* fix(review): a downgraded Request changes must not read as a plain Comment
Fifth self-review, one behavioural finding among five (the rest are test/doc).
verdictLine printed 'Comment — downgraded by a presubmit check' for BOTH a
Suggestion-only Comment the presubmit moved and a REQUEST_CHANGES it moved down to
Comment. The second is a review with confirmed Criticals posted inline, and
'Comment — downgraded' reads to an operator as 'nothing blocking'. It could not
tell them apart from baseEvent alone — a cap may already have softened the RC
before the downgrade ran — so ComposeReviewResult now carries downgradedFrom, and
verdictLine says 'Request changes, downgraded to Comment … (the blockers are still
posted)' for that case. Six verdictLine cases now, including this one.
Also from the same review, all confirmed:
- agent-prompt.test.ts: the describe block named a function that was renamed
(buildRolePrompt -> buildRoleBrief), and the mode-rejection it.each covered 2 of
the invalid combinations, not the role-mode ones; now covers all five and drops
the stale 'two modes' wording.
- SKILL.md Step 7: the review-JSON example used a /* */ comment inside a ```json
fence (not valid JSON); switched to ```jsonc with a // comment.
* fix(review): eight review-round fixes atop the Step-3A invariant resolution
Follows 7c499d193, which resolved the doudouOUC roster finding (a heavy file in
a Step-3A diff must not demand invariant agents — gate the loop on the
topology) and the merge() purity nit. This carries the rest of the same round:
- `roster.ts` requires Agent 0 only for a positive PR number. `!== undefined`
let `null`/`0`/`''` through. Note the reviewer's suggested `typeof === 'number'`
is wrong for this codebase — `fetch-pr` writes the number as a *string* — so
the guard accepts a numeric string too, or every real PR review would lose
Agent 0. A table test pins both directions.
- `transcripts.ts` matches the diff path as a whole JSON string value, so
`…/diff.txt.bak` no longer counts as reading `…/diff.txt`. It also documents
why FIFO is right for a chronological transcript.
- `agent-prompt.ts` scopes path rules to `--file` only for invariant roles — a
whole-diff reviewsCode agent passed `--file` would otherwise lose the rules
for every other file — and guards each chunk element in `diffReadingBlock`
like `chunkFrom`, so a corrupted chunk errors legibly instead of emitting
`offset=NaN`.
- `compose-review.ts` stops double-wrapping `cov.missingRoles` /
`cov.rewrittenPrompts`, which coverage.ts already writes self-explanatory.
- `agent-briefs.ts` JSDoc said "Two do not" read the diff; only Build & Test
does not.
- The agent-prompt size-bound test now covers `test-matrix`.
The empty-prompt guard, the paged-read coverage, the verdictLine dangling-colon
and the submit help text were all already handled by earlier commits on the
branch; those threads are answered without a code change.
* feat(web-shell): auto-post visual previews (screenshots + flow GIFs) on PRs
PRs that touch the web-shell UI now get an auto-updated comment with
light/dark screenshots of key views (transcript, slash menu, model/theme
dialogs, permission panel) and short GIF recordings of common flows,
rendered against the existing mock daemon — no real backend, no secrets.
Split into two workflows for security, since capture runs untrusted PR code:
- web-shell-visuals.yml (pull_request): checks out the PR head, builds and
renders it with Playwright, captures PNGs + webm, converts webm->GIF with
ffmpeg, and uploads an artifact. `contents: read` only, references no
secrets — fork PRs run with a read-only token and no secrets.
- web-shell-visuals-publish.yml (workflow_run): downloads the artifact,
binds it to its real PR by requiring the PR head SHA to equal the run's
authenticated head SHA, hosts the images on a per-PR `pr-assets/*` branch
(referenced by immutable commit SHA), and posts/updates one inline
comment. Never checks out or runs PR code; the write token lives only here.
Capture infra is self-contained in packages/web-shell
(playwright.visuals.config.ts + client/e2e/visuals/*), reusing the mock
daemon harness. Run locally with:
`npm run test:e2e:visuals --workspace=packages/web-shell`.
* fix(web-shell): guard empty gh api response in visuals publish
Addresses review feedback on #6880: if `gh api` returns empty (network
error / rate limit), jq on empty stdin errors and `set -e` kills the
publish job. Skip gracefully instead.
* fix(web-shell): address review nits on visuals capture
- harness recordFlow: wrap video saveAs/delete in try/catch so a video
I/O error (e.g. drive failed before navigation) can't mask the real
driveError.
- capture workflow: drop the unused head_sha.txt artifact field; the
publish job binds to the authenticated workflow_run.head_sha, and an
artifact-sourced SHA would be untrusted.
* fix(web-shell): address second review round on visuals capture
- context.close() in recordFlow's finally is now best-effort (try/catch)
so a close/crash error can't mask the real driveError.
- add a flows spec that asserts a throwing drive propagates its own error.
- trigger the capture workflow on playwright.visuals.config.ts changes too.
* fix(web-shell): address third review round on visuals capture
- harness: log (don't silently swallow) a video save/null when drive
succeeded; keep masking-suppression only when driveError is set.
- publish: HTML-escape interpolated values in the comment builder (defense
in depth, independent of the upstream filename sanitization); fix the
stale 'single pr-assets branch' comment and key concurrency on source
repo+branch so different PRs (incl. same-named fork branches) parallelize.
- capture: bump checkout to v6.0.3 (repo standard); surface ffmpeg's stderr
on GIF-conversion failure instead of discarding it.
* fix(web-shell): harden visuals publish/capture (review round 4)
Publish (privileged workflow_run):
- CRITICAL: capture basename before `tr` so its trailing newline isn't
turned into `_` (which broke the .png/.gif filter -> empty preview).
- dedup only against the bot's OWN comment (author + marker), not any
marker-bearing comment a participant can post.
- bound the pr-assets branch: force-push a single orphan snapshot per run
(previous snapshot GC'd) instead of appending unbounded untrusted content;
this also removes the rebase/retry path.
- cap EXAMINED candidates (not just accepted) before validation; tighten
per-file (3MiB) and accepted-image (14) caps.
- re-validate PR open + head-SHA immediately before the comment write
(TOCTOU); retry the comment listing and abort rather than POST a duplicate
when listing fails.
- esc() the runUrl for consistency with the self-defending HTML.
Capture (pull_request):
- upload raw recordings as a SEPARATE artifact the publisher never downloads,
so an untrusted multi-GB video can't exhaust the privileged job.
- also trigger on packages/webui/src and packages/sdk-typescript/src (the
visuals dev server aliases them).
- create screenshots/gifs dirs before the metadata counts (defensive).
Harness recordFlow:
- track drive failure with an explicit boolean (handles `throw undefined`);
discard the recording on failure so a failed flow leaves no bogus webm.
* refactor(web-shell): extract + unit-test the visuals publish staging/comment
Addresses the review's testability gap (the class of bug that let the
filename sanitizer break the whole preview slip through green CI). The image
validation (magic bytes, filename sanitization, examined/accepted/size caps)
and the comment builder (light/dark pairing, flow labels, HTML escaping) move
from inline workflow bash/node into .github/scripts/web-shell-visuals-publish
.mjs, covered by web-shell-visuals-publish.test.mjs (run in ci.yml's
node --test line). The publish workflow sparse-checks-out and calls the
script instead. Behaviour is unchanged; it just gained a test surface.
* fix(web-shell): retry the visuals asset force-push; drop stale comment
Round-4 switched hosting to a force-push but left a comment referencing a
'push-retry loop' that no longer existed, and the force-push was a single
call that set -e would abort on a transient failure. Add a bounded retry and
correct the comment.
* fix(web-shell): harden visuals publish/capture (review round 6)
Script (unit-tested):
- flow labels: own-property lookup so `toString.gif`/`constructor.gif` can't
leak Object.prototype members into the comment.
- per-kind image caps (screenshots vs gifs) so a large screenshot set can't
silently starve the flow GIFs from the preview.
- tests for both, plus the per-kind cap.
Publish:
- bind the artifact PR number to the run's authenticated head repo+branch
(not just head SHA), rejecting a sibling PR that shares the same commit.
- re-validate before the force-push and again right before the comment write
(close the download/stage/lookup TOCTOU windows).
Capture:
- bound artifact contents before upload (drop oversized / excess files) so an
untrusted spec can't bloat the published or video artifact.
- trigger on the capture workflow file itself.
- new close-trigger cleanup workflow deletes a PR's asset branch on close, so
pr-assets/* refs don't accumulate without bound.
- single-source the capture viewport (constants.ts) shared by config + harness.
- model-switch flow asserts the daemon model request actually fired.
* fix(web-shell): stricter visuals error handling (review round 7)
Harness recordFlow:
- when the drive SUCCEEDS, a failed context.close() or video.saveAs() (or a
missing recording) now FAILS the flow instead of a swallowed console.warn —
a silent pass with no .webm makes the downstream GIF step fail confusingly.
A drive FAILURE still discards the partial video and rethrows the original
error (unchanged).
Publish:
- validate_pr distinguishes a transient API failure (empty after retries ->
exit 1, re-triggerable) from a genuine invalid state (closed / head mismatch
-> skip), via a `gate` wrapper used at all three checkpoints.
- add a 2s backoff between comment-listing retries (matching the push retry).
---------
Co-authored-by: wenshao <wenshao@example.com>
* feat(cli): add /learn command for user-initiated skill creation
Add a /learn slash command that lets users explicitly point the agent at
knowledge sources (local dirs, URLs, conversation history, or freetext)
to create reusable skills.
Implementation uses submit_prompt (same pattern as /remember) to let the
main model gather knowledge and write a SKILL.md in the normal turn.
Skills are saved under .qwen/skills/learned-skill-<name>/ with
source: learned frontmatter for edit isolation from auto-skills and
user-authored skills.
Also includes a forked-agent path (runLearnSkillByAgent) with scoped
permissions for future ACP/auto-learn scenarios, plus 29 unit tests.
* fix(i18n): add zh/zh-TW translations for /learn command description
CI strict-parity check requires all built-in command descriptions to
have translations in zh-CN and zh-TW locales.
* fix(i18n): add en.js baseline key for /learn command description
The check-i18n script requires all translated keys to also exist in the
English baseline locale file.
* refactor(core): remove unused forked-agent infrastructure, add collision guard
Address review feedback on PR #6440:
- Remove ~300 lines of unused forked-agent code (runLearnSkillByAgent,
createLearnSkillScopedAgentConfig, scoped permissions, system prompt,
buildLearnTaskPrompt) — no callers in production code.
- Make buildLearnSkillPrompt async and add listExistingSkillDirNames
enumeration to prevent skill name collisions.
- Revert buildAgentHistory export (no longer needed).
- Trim test suite from 29 to 10 tests (dead code tests removed).
* feat(config): default auto-skill to disabled
With /learn available for explicit skill creation, auto-skill (background
skill extraction after 20 tool calls) is no longer needed as the default
experience. Users who want automatic extraction can re-enable it via
memory.enableAutoSkill in settings.
* feat(config): align auto-skill default to disabled across all layers
The prior commit only changed the core Config constructor default.
CLI config builder, ACP agent defaults, and settings schema still
defaulted to true, making the core change unreachable for CLI users.
Align all layers:
- cli/config.ts: ?? true → ?? false
- acpAgent.ts: enableAutoSkill default true → false
- settingsSchema.ts: default true → false
- read-file.ts: resolve stale merge conflict markers
* chore: regenerate settings.schema.json for auto-skill default change
* test: update tests for auto-skill default change, fix stale conflict resolution
- config.test.ts / acpAgent.test.ts: update assertions for the
enableAutoSkill default flip (true -> false) from the prior commit.
- read-file.ts: an earlier rebase conflict resolution incorrectly kept
os.tmpdir() as an allowed read root — it was removed on main. Restore
the main version so read-file.test.ts's "ask for OS temp paths" case
passes again, and drop the now-unused os import.
* fix(core): restore dynamic ignore-file message, wrap /learn input as opaque data
- read-file.ts: an earlier rebase conflict resolution incorrectly hardcoded
the ignore-file error message to '.qwenignore', losing the dynamic
getQwenIgnoreFileDisplayForPath() call that reports the actual file
(.agentignore, .cursorignore, etc.) that blocked the read. This was the
real cause of the read-file.test.ts CI failures — not a pre-existing bug
as previously assumed.
- learn-skill-agent.ts: wrap the /learn command's raw user input in
<user_data> tags with an explicit "don't follow instructions" preamble,
matching the existing <user-content> pattern in remember.ts. Since
buildLearnSkillPrompt feeds submit_prompt (full tool access), unwrapped
content fetched from a URL could otherwise redirect tool calls.
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix(webui): route useLocalStorage functional updates through prev state
setValue applied a functional updater to the closed-over storedValue instead
of React's previous state, so two setValue(fn) calls batched in one render
both derived from the same base — the first update was lost and the
stale-derived value was persisted to localStorage. Route the update through
the setStoredValue(prev => …) form and persist the committed value. Adds a
jsdom regression test for the batched-update case.
* refactor(webui): persist via effect and guard against throwing updaters
Address review:
- Restore error handling around the functional updater: a throwing
updater is caught and leaves state unchanged (previously it could
propagate through render and crash the tree).
- Move the localStorage write into an effect so the state updater stays
pure (StrictMode-safe), keeping a first-run baseline ref so the initial
value is never written on mount.
- Add tests for the no-mount-write contract, invalid-JSON hydration
fallback, and state updating when setItem throws.
* feat(cli): change default approval mode from default to auto
The default approval mode required manual confirmation for every tool
call, producing dozens of confirmation prompts per task. Auto mode uses
a three-layer filter (workspace edits, read-only allowlist, LLM
classifier) to auto-approve safe operations while still guarding risky
ones.
Untrusted folders are still forced to default mode for safety.
Closes#6898
* fix(cli): keep manual approval in safe and bare modes
Restricted modes (safe/bare) strip permissions, allowlists, MCP servers
and hooks to provide a maximally restrictive session. The new AUTO
default fallback was silently downgrading them to the LLM classifier,
contradicting their lockdown intent. Restore DEFAULT (manual approval)
for these modes while keeping AUTO as the default for normal sessions.
Explicit --approval-mode and --yolo flags still take effect, since they
are resolved before the fallback.
* chore(cli): regenerate settings schema for auto default
Regenerate the VS Code settings schema so the tools.approvalMode default
matches the new auto value (fixes the "settings schema is up-to-date" CI
check). Also add coverage for the serve-mode approval fallback when no
approval mode is configured.
* test(cli): update SettingsDialog snapshots for auto approval default
The settings schema now defaults tools.approvalMode to auto, so the
SettingsDialog renders "Auto" instead of "Ask permissions" for the Tool
Approval Mode field. Regenerate the affected snapshots (10 updated).
* test(core): pin DEFAULT baseline in agent-override tests
These tests exercise createApprovalModeOverride isolation and the
DEFAULT→AUTO rule strip/restore transitions, so they implicitly relied
on the Config constructor defaulting to DEFAULT. Now that the default is
AUTO, pin the baseline explicitly so the tests no longer depend on the
constructor default.
---------
Co-authored-by: pomelo.lcw <pomelo.lcw@alibaba-inc.com>
* fix(cli): apply FETCH_TIMEOUT_MS to /update version check and log fetchInfo results (#6857)
The FETCH_TIMEOUT_MS = 2000 constant in updateCheck.ts was defined but
never wired up. update-notifier's fetchInfo() takes no timeout option,
so slow/unreachable registries (corporate proxies, offline networks,
scoped .npmrc mirrors without auth) would either hang the check or fall
back to whatever update-notifier internally decides — sometimes a stale
configstore cache, reported by users as '/update reports up-to-date on
0.19.9 when 0.19.10 is available.'
Race fetchInfo() against a bounded timer via Promise.race and surface a
new UpdateCheckTimeoutError when it fires, so '/update' returns the
existing 'error' status instead of silently reporting 'up to date.'
Also log the fetchInfo return value under the UPDATE_CHECK debug tag so
the next round of reports can distinguish 'registry returned the wrong
version' from 'we compared incorrectly' without adding more speculation.
Refs #6857
* fix(cli): carry dist-tag on UpdateCheckTimeoutError and cover nightly timeout paths
Address bot review on #6887:
- `UpdateCheckTimeoutError` now takes an optional `distTag` argument that
is threaded through by `fetchInfoWithTimeout` and appended to the
message. The nightly path fires `nightly` and `latest` fetches
concurrently via `Promise.all`; without a dist-tag on the error, an
oncall reading logs cannot tell which registry endpoint stalled (e.g.
a corporate proxy that lets `nightly` through but blocks `latest`).
The tag also lands on the error instance as a public `distTag` field
so callers can branch on it programmatically.
- Add two regression tests for the nightly `Promise.all` timeout path:
a single stalled dist-tag (asserts Promise.all propagates the timeout
and names the exact tag) and both stalled (full outage — asserts we
still surface a typed error with a valid tag). The non-nightly test
now also asserts the message contains `for latest`.
* feat(cli): add general.notificationMode to silence per-approval notifications (#6898)
Users driving many tool approvals in a single task report getting "几十次
弹窗" per session because the current `general.terminalBell` toggle
fires on both WaitingForConfirmation transitions AND long-task idle.
The reporter (and the triage) explicitly wanted a task-complete-only mode.
Add a new `general.notificationMode` enum setting with two values:
- `all` (default): fire on every approval prompt AND on task completion —
identical to the pre-#6898 behavior. Legacy configs and unrecognized
values (typos, future keys) also fall back to this to avoid silently
suppressing notifications.
- `task-complete`: suppress the per-approval notification. The long-task
idle notification still fires, so users who leave the terminal
unfocused still learn when a task finishes.
Gate the WaitingForConfirmation branch in `useAttentionNotifications` on
the mode. Register the setting in the acp agent's typed metadata and in
the workspace-settings TUI allowlist so it plumbs through the same paths
as the existing `terminalBell`.
Four regression tests pin: task-complete suppresses approval, task-
complete still fires task-completion, explicit `all` is a no-op vs
unset, and an unknown value falls back to `all`.
* chore(cli): regenerate settings.schema.json for general.notificationMode
CI's 'Check settings schema is up-to-date' step runs
`npm run generate:settings-schema` and diffs — the new enum setting
added in the previous commit was missed. Regenerated.
loadTrustedFoldersWithOverrides() mutated the module-cached
LoadedTrustedFolders singleton via `folders.user.config = trustConfig`.
Callers pass an override to preview trust status for a tentative config
(useTrustModify's updateTrustLevel builds it "to check the new trust
status without writing"), so the unconfirmed config leaked into every
subsequent loadTrustedFolders() read and could be persisted to disk on
the next setValue().
Return a fresh LoadedTrustedFolders with a spread-copied config for the
override case instead of mutating the singleton. Adds a regression test
asserting the cached config is unchanged after a preview override check.
Fixes#6831
* fix(vscode-companion): don't let a non-boundary @ suppress / completion
An @ before the cursor unconditionally took priority over /, so an @ that
is not at a word boundary (e.g. inside an email like foo@bar.com) was
selected, failed the boundary check, and short-circuited before the / was
ever considered. Typing 'contact foo@bar.com /he' therefore showed no
slash-command completion.
Extract the trigger resolution into a pure resolveCompletionTrigger() that
only treats an @ as a trigger when it is at a word boundary, otherwise
falling through to a valid /. Adds unit tests for the resolver.
* test(vscode-companion): cover newline @ boundary and non-boundary /
Address review: add coverage for the two untested branches in
resolveCompletionTrigger — a newline as an @ word boundary, and a /
that is not at a word boundary (with no valid @) resolving to null.
* test(vscode-companion): cover @-over-/ priority at word boundaries
Address review: add a test pinning the central design decision — when
both @ and / are at valid word boundaries, @ wins. Without it, swapping
the branch order would pass every other test.
* fix(vscode-companion): accurate image-size messages and formatFileSize units
Two small fixes in the paste-image flow:
- The "too large" error hardcoded "Maximum size is 10MB" while the check
uses MAX_IMAGE_SIZE; derive the message from the constant so it can't drift.
- formatFileSize used ['B','KB','MB','GB'] with an unclamped unit index, so a
>= 1 TB value read sizes[4] === undefined and rendered "… undefined". Add
'TB' and clamp the index to the array bounds.
Exports formatFileSize and adds unit tests (including the TB regression).
* refactor(vscode-companion): drop self-evident clamp comment in formatFileSize
Address review: the Math.min(..., sizes.length - 1) clamp is self-evident;
per AGENTS.md, comments default to none unless the why is non-obvious.
* fix(cli): keep exit_plan_mode plan visible inside the pending viewport clamp (#6867)
The exit_plan_mode confirmation dialog rendered its plan body via
MarkdownDisplay with isPending={false}, which skipped fitPendingSlice.
The full plan then rendered inside MainContent's maxHeight +
overflow="hidden" wrapper (re-added by #6421 as an Ink backstop for the
scroll-to-top lock), and Ink clipped the bottom — silently dropping the
tail of a long plan and, in narrower terminals, the option buttons.
Add an opt-in enforceHeightBudget flag on MarkdownDisplay so callers
that render inside a bounded parent can share the streaming path's
rendered-height slice. Pass enforceHeightBudget from
ToolConfirmationMessage's plan body. Committed non-pending renders
(transcript, tool result markdown, PlanSummaryDisplay) keep the default
uncapped behavior.
Fixes#6867
* fix(cli): render truncation cue when exit_plan_mode plan is clipped
Address bot review on #6882: when `enforceHeightBudget && !isPending`
and the pre-slice actually dropped lines, render a dim single-line cue
naming the count of dropped source lines. Without a visible cue, a
model-authored plan could hide dangerous steps past the viewport budget
and users would approve them blind — the streaming path deliberately
omits the cue because the tail is still on its way, but a complete plan
inside the confirmation dialog has no such promise.
The cue is gated so it only appears in the exact scenario it's needed:
enforceHeightBudget on (only exit_plan_mode opts in today), non-streaming,
and something actually got dropped. Three regression tests pin each gate.