mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-10 09:15:24 +00:00
3503 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7c1a93477c
|
perf(review): scope Agent 7's build/test to the workspaces the diff changed (#6955)
* 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. |
||
|
|
19fc52aa93
|
feat(daemon): add stateless generation SSE (#6947)
* feat(daemon): add stateless generation SSE * test(integration): expect session generation capability * fix(daemon): address generation review findings * fix(daemon): harden generation regressions * fix(daemon): preserve generation error events --------- Co-authored-by: ytahdn <ytahdn@gmail.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
859095bc98
|
feat(channels): support natural memory references (#6952)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
* feat(channels): plan targeted memory intents * fix(channels): harden targeted memory planner * fix(channels): bound serialized memory manifest * feat(channels): resolve natural memory references * fix(channels): harden memory intent routing * docs(channels): explain natural memory references * fix(channels): type resolved memory intent dispatch * fix(channels): route Chinese preference filters |
||
|
|
0d136bd132
|
feat(review): prove Step 4 (verify) and Step 5 (reverse audit) actually ran (#6965)
* 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. |
||
|
|
7a1b182cd1
|
feat(cli): Add archived session export (#6911)
* feat(cli): add archived session export Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6911) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6911) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6911) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6911) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
fa1c402c66
|
feat(review): build the Step 4 verifier and Step 5 reverse-audit prompts in code (#6942)
Some checks failed
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Python / Classify PR (push) Has been cancelled
SDK Python / SDK Python (3.10) (push) Has been cancelled
SDK Python / SDK Python (3.11) (push) Has been cancelled
SDK Python / SDK Python (3.12) (push) Has been cancelled
* 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).
|
||
|
|
bad7d6ba10
|
fix(cli): isolate submit tests from inherited QWEN_CODE_SESSION_ID (#6940) (#6944)
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> |
||
|
|
14993b1cf3
|
feat(daemon): add immutable session source metadata (#6932)
* feat(daemon): add session source metadata * test(daemon): update baseline capabilities --------- Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
35c9186441
|
fix(review): prove the diff was read, build every agent's prompt, and compute the verdict (#6892)
* 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. |
||
|
|
9cb09f4e2e
|
fix(core): Classify shell timeouts as tool errors (#6864)
* fix(core): classify shell timeouts as tool errors Refs #6863 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6864) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): Report no output for sed timeouts Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
24b2442875
|
feat(cli): add /learn command for user-initiated skill creation (#6440)
* 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> |
||
|
|
ca5019968a
|
fix(web-shell): harden non-primary archive actions (#6912)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
41157205c1
|
fix(config): reject fractional session and tool-call limits (#6920)
* fix(config): reject fractional session and tool-call limits * fix(config): validate persisted session turn limits |
||
|
|
389a1f9ceb
|
feat(cli): change default approval mode from default to auto (#6899)
* 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> |
||
|
|
0957e18ea2
|
fix(cli): apply FETCH_TIMEOUT_MS to /update version check and log fetchInfo results (#6857) (#6887)
* 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`. |
||
|
|
1a56193bd5
|
feat(cli): add general.notificationMode to silence per-approval notifications (#6898) (#6922)
* 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. |
||
|
|
95b7d750ae
|
fix(cli): don't mutate cached trusted-folders config on preview trust check (#6900)
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 |
||
|
|
4f4387cf57
|
feat(core): add PDF vision bridge fallback (#6846)
* feat(core): add PDF vision bridge fallback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6846) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6846) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: fix CI failure on PR #6846 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6846) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6846) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): harden vision bridge output handling Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): correct export sanitizer test typing Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): disclose selected vision endpoint before egress Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
82ba1d3e2d
|
fix(cli): keep exit_plan_mode plan visible inside the pending viewport clamp (#6867) (#6882)
* 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. |
||
|
|
fb0239eab4
|
feat(channels): add structured channel memory management (#6860)
* feat(core): add structured channel memory document * fix(core): preserve legacy channel memory whitespace * feat(core): structure channel memory storage * fix(core): close channel memory races * test(core): cover channel memory read failures * feat(channels): parse channel memory item intents * test(channels): cover memory intent precedence * feat(channels): manage structured channel memory * fix(channels): address structured memory review * feat(cli): wire structured channel memory * docs(channels): document structured channel memory * fix(core): harden channel memory persistence * fix(core): preserve channel memory uniqueness * fix(channels): prioritize channel memory item updates |
||
|
|
a596808c65
|
fix(vscode): run ACP process in Electron Node mode (#6866)
* fix(vscode): run ACP process in Electron Node mode * docs(vscode): clarify electron node mode * test(vscode): reset acp spawn mock * fix(vscode): scrub electron bootstrap env |
||
|
|
cf42ab6b7e
|
feat(acp): expose tool-call preparation lifecycle (#6819)
* feat(acp): expose tool-call preparation lifecycle Why: ACP clients receive no signal while providers stream tool arguments, making long calls appear stalled and delaying tool-identity policy decisions. What: - attach transient preparation metadata for Anthropic and OpenAI-compatible streams - emit correlated ACP pending, execution, and discarded lifecycle updates - preserve normalized call IDs across partial chunks and provider ID reuse - clear abandoned retry calls and keep cleanup failures from terminating healthy retry/fallback streams - deduplicate suppressed preparations and protect completed remapped parser buffers - cover multi-tool Anthropic streams, ID reservation, TodoWrite suppression, retry cleanup, cancellation, and stream failure Impact: The metadata is additive and consumed only by ACP. It exposes no partial arguments, is not persisted to conversation history, and does not move permissions, hooks, scheduling, or execution ahead of complete function calls. Tests: - Core provider and stream suites: 649 passed - ACP lifecycle suites: 316 passed - npm run build - npm run typecheck - npm run lint:ci - changed-file Prettier and git diff checks Refs: #6775 * fix(acp): stabilize tool preparation lifecycle updates Why: - ACP cleanup failures must not convert a successful model stream into a failed prompt. - A prepared tool call must be updated in place when execution starts instead of creating a second card. What: - Preserve the primary stream outcome when preparation cleanup fails and remove duplicate message display finalization. - Track prepared call IDs so execution starts use tool_call_update, guard empty preparation metadata, and cover late stable IDs. Impact: - Ordinary tool calls keep their existing tool_call start frame. - Streaming parser production behavior is unchanged. * Update packages/cli/src/acp-integration/session/Session.test.ts overrides参数在createPreparationResponse中被声明但从未使用——所有11个调用点仅传递callId且toolName. 该as GenerateContentResponse强制类型转换会绕过对始终为空对象的结构化类型检查。 Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com> * fix(acp): harden preparation lifecycle handling Why: - A malformed test helper prevented the preparation lifecycle suite from compiling. - Duplicate preparing frames and state cleanup need direct regression coverage. What: - Repair the preparation response helper and isolate cleanup warning assertions. - Suppress duplicate preparing frames and cover terminal cleanup plus missing tool call IDs. Impact: - Normal preparation and execution transitions remain unchanged. - Repeated preparation frames for the same call ID are now ignored. --------- Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com> |
||
|
|
d7e2892a7c
|
fix(cli): avoid updating active CLI processes (#6874)
* fix(cli): avoid updating active processes * fix(cli): close update relaunch gaps * test(cli): fix standalone update source path * fix(cli): reset deferred update per relaunch |
||
|
|
ec57b1d272
|
fix(cli): wrap long compact tool summaries (#6847)
* fix(cli): wrap long compact tool summaries * fix(cli): account for wrapped compact summaries * fix(cli): make compact summary height estimation robust * fix(cli): reserve timeout width in compact summary estimate |
||
|
|
61deedf77c
|
feat(cli): VP mode UX improvements (#6885)
* feat(cli): make VP mode banner scroll with content instead of pinning at top The banner (logo + model info + tips) was fixed at the viewport top, wasting ~7 lines on small terminals. Inject it as the first item in the virtual scroll list so it scrolls away as content grows, reclaiming space for actual conversation history. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): use layoutRowForEvent in ClickableThinkMessage hit-test The click-to-expand handler on collapsed thought blocks used a bare `event.row - 1` for coordinate mapping. When the ink frame exceeds the terminal height (e.g. ShowMoreLines renders an extra row), the frame anchor becomes negative and the naive mapping always misses the 1-row-tall collapsed thought. Adopt the same layoutRowForEvent helper that RowMouseController uses, which correctly accounts for the frame anchor offset. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): use type discriminant for banner sentinel + top-anchor empty session - Use `item.type === 'vp-banner'` uniformly in virtualKeyExtractor and virtualIsStaticItem instead of comparing against VP_BANNER_ID - Add comment documenting that index 0 is the banner sentinel in virtualEstimatedItemHeight (positional check is an API limitation) - Anchor to top (index 0) when banner is the only item, preventing the logo from being clipped off in short terminals on fresh sessions Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
c538bd70d2
|
feat(core): emit liveness heartbeats for silent foreground shell commands (#6876)
* feat(core): emit liveness heartbeats for silent foreground shell commands Silent foreground commands previously produced no events between spawn and settle, so ACP gateways and stream-json consumers could not tell a long-running command from a dead session. The shell tool now emits a structured ShellProgressData through the existing updateOutput channel whenever no display update has fired for tools.shell.heartbeatIntervalMs (default 10s, 0 disables). Heartbeats carry liveness stats only - never command output - and never enter model context. Consumers: the ACP session forwards heartbeats as meta-only tool_call_update frames (gated so a tick racing the settle path cannot regress status after completion) and records heartbeat span attributes; stream-json forwards them as tool_progress events behind includePartialMessages; the TUI scheduler, React hook, and subagent runtime ignore them so live output views are not replaced by stats objects. * docs(design): add silent command heartbeat design doc * fix(acp): keep tool_call_update heartbeats from breaking in-repo consumers Codex review of the heartbeat change found that in-repo ACP consumers did not tolerate the new meta-only in_progress frames. A full sweep of tool_call_update consumers found three that mishandled them, each now guarded with a regression test: - The desktop agent converted every tool_call_update into a terminal tool_result, so the first heartbeat would prematurely complete the command with an empty result. It now skips in_progress updates. - DaemonChannelBridge requires kind on tool_call_update and flagged the kind-less heartbeat as a malformed-protocol error every interval. It now drops kind-less in_progress frames silently. - The web-shell daemon UI normalizer derived the tool block title from _meta.toolName, overwriting the human-readable title on every heartbeat. It now drops heartbeat frames outright. The remaining consumers (VS Code companion, acp-bridge compaction, session export, daemon TUI adapter) merge updates conditionally and are heartbeat-safe without changes. * fix(core): address PR review — heartbeat monotonic gate, guard scope, telemetry Review round 1 on #6876 (yiliang114, wenshao, chiga0, qwen3.7-max): - shell.ts: the silent-idle gate now uses the monotonic performance.now() clock (via lastOutputPerfTime, falling back to spawn time) instead of the Date.now()-based lastUpdateTime, so an NTP step can neither skew the payload nor misfire a heartbeat — matching the design doc's monotonic commitment. It also keys off actual output arrival rather than the throttled display update. - session-tracing.ts: endToolExecutionSpan now applies caller-supplied attributes BEFORE the canonical keys (duration_ms, success, error) so a passthrough attribute can never mask the span's own outcome fields. - desktop qwen-agent.ts: the in_progress drop guard is now scoped to frames carrying _meta.shellProgress, matching the daemon bridge and web-shell normalizer guards, so a future non-heartbeat in_progress frame is not silently swallowed. - Tests: the desktop regression test now pins result==='done' (previously it stayed green even with the guard removed); added a Session.test assertion that heartbeat counts reach the tool-execution span attributes. * fix(acp): align desktop heartbeat guard with normalizer; test kind pass-through Review round 2 on #6876 (qwen3.7-max via ci-bot): - The desktop qwen-agent in_progress drop guard was broader than the web-shell normalizer's: it dropped any in_progress + shellProgress frame regardless of kind, while the normalizer only drops kind-less ones. The comment claimed they matched. Added the kind-absent check so the desktop guard matches the normalizer exactly — a kind-bearing frame now passes through on both platforms (heartbeats emitted by the ACP session never carry a kind, so real behavior is unchanged). - Added pass-through tests on both sides (daemonUi + desktop) asserting an in_progress frame WITH a kind normalizes to a tool.update / tool_result rather than being dropped, so the load-bearing kind-absent condition is no longer only exercised on the drop path. * fix(channels): scope daemon bridge heartbeat drop to shellProgress frames Review round 3 on #6876 (qwen3.7-max via ci-bot): the DaemonChannelBridge heartbeat guard lived in the shared tool_call / tool_call_update case and dropped ANY kind-less in_progress frame, so a genuinely malformed kind-less tool_call (status in_progress, no shellProgress) was silently swallowed instead of reaching emitProtocolError. Gate the drop on _meta.shellProgress — matching the qwen-agent and web-shell normalizer guards — so real heartbeats are still dropped while malformed frames are flagged. Added a regression test for the malformed path. |
||
|
|
515a83110a
|
Revert "fix(shell): mark non-zero exits as failed (#6869)" (#6875)
This reverts commit
|
||
|
|
b59b341a0a
|
feat(web-shell): add extension management page (#6815)
* feat(daemon): support interactive extension installs * feat(web-shell): add extension management page * fix(web-shell): align extension update behavior * fix(web-shell): polish extension management UI * fix(extensions): harden interactive operations * fix(web-shell): address extension review suggestions * fix(web-shell): refine extension interaction handling * fix(web-shell): resolve extension operation races * fix(web-shell): harden extension action admission * fix(web-shell): surface extension recovery failures * fix(web-shell): preserve extension card titles * fix(web-shell): refine extension card layout * fix(extensions): address operation review findings * test(extensions): close remaining review gaps --------- Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com> Co-authored-by: ytahdn <ytahdn@gmail.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
4dd80a7c94
|
fix(shell): mark non-zero exits as failed (#6869)
Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
42d7d28d11
|
chore(release): v0.19.10 (#6855)
* chore(release): v0.19.10 * docs(changelog): sync for v0.19.10 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
2ba836cc8f
|
fix(review): prove coverage from the harness's records, not the caller's file (#6843)
* fix(review): build the chunk agent's prompt in code — they were launched blind
The review agents did not whiff. They were never given the diff.
Measured against the harness's own record of what they were actually started with
— the first record of each subagent transcript, written at launch and not
retconnable — 23 of 23 chunk agents got a prompt that named no diff file at all:
no path, no `read_file`, no offset, no limit. All 23 made zero tool calls.
What they got was a *description* of a chunk they had no way to open ("The changes
are in chunk 13 of 23, covering lines 3808-4024 of the diff"), and a sentence to
say if they found nothing ("If you find no issues, say `No issues found —
reviewed chunk 13 (…)`"). Every one of them said it. The receipts that looked
like proof of work were in the prompt that launched them; the coverage machinery
downstream was reading them back.
The skill has always specified what that prompt must contain — the diff path, the
agent's own offset and limit, the paging rule, the uncoverable rule. It said so
in a paragraph, and the orchestrator composed the prompt itself. This is the same
failure the skill has now fixed five times: a rule stated in prose is a rule that
will eventually not be followed, and the fix is to move it into code that can say
no. It was applied to the review target, the posting gate, the verdict and the
coverage report. The agent's own prompt — the thing that decides whether a review
can happen at all — was the one place it was not.
`qwen review agent-prompt --plan <report> --chunk <id>` now builds it. The
orchestrator passes what it prints, verbatim. A plan with no `diffPathAbsolute`
is an error rather than a prompt that merely describes the chunk, because that
prompt is the bug.
The generated prompt deliberately does not hand the agent a stock sentence to
recite when it finds nothing — it asks the agent to name what it examined. A
return that names nothing it read is indistinguishable from never having read
anything, and an agent that cannot open the diff will recite the sentence anyway.
* fix(review): prove coverage from the harness's records, not the caller's file
Every coverage gate this skill has built read a file the orchestrator wrote, and
the orchestrator is the thing being checked.
The first asked it to copy the agents' returns into `returns.txt` and read the
receipts back out. On the next run it fabricated them — invented file lists,
invented `Covered: chunk N lines X-Y` — and the check reported 23 of 23 chunks
covered over a diff nobody had read. The second checked the agents' prose for
evidence of work. Measured against 129 real transcripts, it caught none of the 80
agents that made no tool call at all: every one of them returned more than forty
characters of confident, specific text ("No issues found — reviewed chunk 13
(packages/cli/…) thoroughly"). A verbose whiff is invisible to any check on the
words; it is unmistakable in the tool calls.
Coverage is now computed from the harness's own per-agent transcripts —
`<projectDir>/subagents/<sessionId>/agent-<id>.jsonl`, written by the CLI, one
line per event, opened at launch. The orchestrator does not author it, is never
told its path, and cannot retcon it: the launch prompt is the file's first line,
written before the model has said anything. `returns.txt` is gone, and with it the
surface that was forged.
Three failures, and they are not the same failure:
- **Launched blind.** A chunk agent whose recorded launch prompt never named the
diff file could not have read it. This is reported as itself, because the
remedy is different: relaunching a blind agent produces a second agent that is
just as blind. The prompt is the defect (see `agent-prompt`).
- **Made no tool call.** It read nothing, whatever it wrote.
- **Chunk nobody reviewed.**
And the hop that would have made all of this pointless: `compose-review` used to
take `coverage` as a field inside the JSON the model writes. Hardening the checker
while the composer still believed a hand-typed `{"ok": true}` would have moved the
forgery one step downstream and made it *cheaper* — one object, instead of the
eighteen receipts it actually bothered to fabricate. `compose-review` now
recomputes coverage itself, from the same transcripts. It is given the plan path;
it is not given the answer.
`Config` publishes `QWEN_CODE_PROJECT_DIR` alongside the session id, because the
project dir is keyed on the session's launch cwd and this skill deliberately
`cd`s into a PR worktree — a subprocess recomputing it from `process.cwd()` lands
on a directory that never existed.
Two failures that must not be conflated, and are not: no transcripts at all is an
environment fault (a read-only HOME, a sandbox) and says so; transcripts showing
no work is a finding about the run. Both refuse to certify the diff. Neither is
silent, and neither pretends to be the other.
* fix(review): scope the project dir per session, and stop misreading a working agent as idle
`QWEN_CODE_PROJECT_DIR` was claimed in one process-global slot, exactly the design
that was already wrong for the session id: in daemon mode a single process serves
many sessions, the slot holds whichever booted first, and every later session
would hand its subprocesses another session's directory — where it would look for
that session's transcripts and find none. It is registered per session now, and
resolved through the same async-local session the shell env already uses.
`isErrorResponse` matched `/"error":/` against the whole stringified record, so a
tool whose *response* legitimately carries `error: null` — which means the call
succeeded — was counted as a failed call, and its agent reported as having read
nothing. It reads the response object now.
Also: the coverage summary said "N agent(s) that opened the diff" while counting
every transcript, including the blind and the idle ones — the two things the
report exists to tell apart. It now says how many ran, how many opened the diff,
and how many did neither. `check-coverage`'s args interface no longer declares
three fields the builder does not define and the handler never reads.
Two test defects, and the second is the more embarrassing: a fixture wrote the
transcripts before the plan and relied on `mtime <` between them, which flips at
random when both land in the same millisecond. And the test meant to prove that a
tool output containing `"error":` is not a failure used a payload that JSON-escapes
to `\"error\"` — so it never exercised the coarse match it was written to forbid,
and a mutation reintroducing that match survived it. Caught by mutation testing;
the payload now carries a real `error: null` field.
* fix(review): remove the Step 3B section I duplicated, and stop calling a blind agent sighted
The previous commit spliced ~95 lines of Step 3B into the skill twice — the
whole-diff agents, the whole-file invariant agents, and the coverage step, each
appearing in two slightly different wordings. The orchestrator would have read
both. It is one section again, and the file's headings now match main exactly.
`wasGivenTheDiff` accepted any prompt containing `read_file(`, which proves only
that *some* file was named. A prompt that points an agent at source files and
never at the diff is exactly as blind as one that names no file at all — it would
have passed as "not blind", and its silence would then have been reported as a
whiff, sending the reader off to relaunch an agent whose prompt is the defect. It
requires the diff path now, and nothing weaker.
The chunk-id validation that lived in the old `runCheckCoverage` was lost when
coverage moved into its own module: a non-integer or duplicate id silently never
matches a launch prompt, so the chunk it stands for is reported unreviewed
forever. Restored, with tests.
Also: a coverage-derived uncoverable chunk that the caller had already named
rendered twice ("Not reviewed: chunk 5, chunk 5"), and the input type still
carried a JSDoc block describing the `coverage` field that this branch deletes.
* fix(review): strip a model-supplied env, and check idle before uncoverable
Two holes a review found, both mine.
`compose-review`'s CLI handler parsed the model-authored JSON and passed it whole
to `composeReview`, including an `env` field the code's own comment swore "a model
cannot" supply. It could: `env` decides where the harness transcripts are read
from, so a model that wanted an approval could point it at a directory of
transcripts it fabricated — the entire gate reopened through one extra key. The
handler now deletes `env` before composing; it is a unit-test seam and the real
run always resolves transcripts from the environment the CLI exported.
The coverage predicate checked an agent's `Uncoverable: chunk N` claim before it
checked whether the agent had made any tool call. But `Uncoverable` is a line the
prompt hands the agent, and an honest one requires having read the chunk to find
the line too long. A zero-tool-call agent that copied the template was credited
with a disclosed gap — the whiff in a costume. The idle check runs first now, and
`ok` excludes uncoverable chunks (a diff with a line no read can reach was not
reviewed, and must not approve on its strength).
Tool-call detection read the message parts structurally instead of matching
`"functionCall"` as a substring of the serialized record, the stderr summary says
"did work" rather than overclaiming "opened the diff" for the build/issue agents
that never touch it, and the per-session project-dir map gained an unregister path
so a daemon does not leak an entry per session.
Two of the tests I first wrote for this were vacuous — the env one couldn't tell
a stripped env from an honoured one until the forged directory held a
transcript that WOULD have approved, and the ordering fix needed a zero-call agent
that copied the Uncoverable line. Caught by mutation.
* fix(review): actually wire the session-dir cleanup, and diagnose an uncoverable cap
Last round added `unregisterSessionProjectDir` and a test for it, and then never
called it — so the daemon leak it was meant to fix was still open. `Config.shutdown`
calls it now, with a test that constructs a Config, asserts its entry is present,
shuts it down, and asserts the entry is gone. Mutation-verified: dropping the call
turns the test red.
`check-coverage` set exit 3 for an uncoverable chunk but printed no reason for it,
so a run that failed solely on that gave the user a bare non-zero exit — every
other failure has an ERROR line; this one now does too.
The uncoverable dedup compared `chunk 5` against a caller's richer `chunk 5
(src/big.min.js)` with strict equality, which never matched, so both rendered
("Not reviewed: chunk 5, chunk 5"). It compares by the `chunk <id>` prefix now.
Removed `toolCalls` and `file` from `AgentRecord` — nothing read them; only
`successfulToolCalls` is used. Added a dedicated `transcripts.test.ts` for the
reader's defensive branches (unreadable/empty/non-transcript file, a half-written
final line, a failed tool call not counted as work), and the `r.ok` assertion the
uncoverable-declaration test was missing.
|
||
|
|
1f0078c7a2
|
feat(serve): Add workspace-qualified session export (#6844)
* feat(serve): add workspace-qualified session export Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6844) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
c7250df8ea
|
feat(serve): Add workspace-qualified Voice (#6839)
* feat(serve): add workspace-qualified voice Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): harden workspace voice lifecycle Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6839) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6839) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): address workspace Voice review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6839) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): clean up Voice lifecycle resources Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): address Voice review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
57e147b136
|
refactor(review): share the probe-worktree path helper; harden the stale-tree sweep (#6841)
* refactor(review): share the probe-worktree path helper; harden the stale-tree sweep Follow-up to the disposable-probe-worktree change (#6836), addressing review suggestions on it. - Extract `probeWorktreePath(worktree)` into `lib/paths.ts` and call it from both the probe (`test-efficacy.ts`) and `cleanup.ts`. The `-probe` suffix was constructed independently in the two files; renaming it in one and missing the other would silently stop cleanup from sweeping. The helper also settles the normalisation: it returns an absolute path, because the probe drives `git worktree add`/`remove` with the shared worktree as cwd, where a relative path would resolve against that worktree and nest the probe tree inside it. - Make the pre-`add` sweep clear an UNREGISTERED leftover too. `git worktree remove --force` only unregisters a tree git still tracks; a directory left at the probe path after metadata loss or a partial cleanup is reported as "not a working tree" and left in place, and a non-empty one then makes `worktree add` fail `already exists` — wedging every probe as `inconclusive` until it is cleared by hand. Follow the unregister with an `rmSync` of whatever remains (which unlinks a symlink rather than following it), so `add` always gets a clean path. The sweep's stderr is now kept and appended to the create-failure detail so a genuine `add` failure explains itself. Tests: `probeWorktreePath` unit tests (suffix, relative→absolute, shared source); integration tests that a stale REGISTERED probe tree is swept and the probe still runs, and that an UNREGISTERED non-empty leftover is cleared rather than wedging the probe. Full review suite green (416). * fix(review): make releaseWorktree actually remove an unregistered leftover Follow-up review on #6841 pointed out that the `rmSync` fallback added to the probe's own sweep was missing from `cleanup.ts`, which sweeps the probe worktree through `releaseWorktree`. That helper returns `existed` (true when the path was there on entry) but only runs `git worktree remove --force` — which does nothing to a directory git no longer tracks as a worktree. So an unregistered non-empty leftover survives while `cleanup.ts` prints "Removed probe worktree", and it still blocks the next `git worktree add` with `already exists`. Fix it at the root: `releaseWorktree` now `rmSync`s whatever remains after the unregister, so a `true` return means the path is actually gone. This covers the probe sweep and the main review-worktree sweep in one place, rather than duplicating the fallback at each call site. `rmSync` unlinks a symlink instead of following it, so a tampered leftover cannot redirect the delete. Test: `git.integration.test.ts` — an unregistered non-empty leftover is removed and the path is add-able again. * refactor(review): apply the leftover-safe sweep to the probe's finally too More review on #6841. Two accuracy/consistency fixes: - The probe's post-run `finally` still discarded the tree with the throwing `git()` wrapper only — the one sweep site that had not learned the unregistered/non-empty-leftover lesson the pre-sweep and `releaseWorktree` already did. Give it the same two steps (`worktree remove` best-effort, then `rmSync` whatever remains) and key `cleanupFailure` off whether the path still exists afterward, which is the honest signal. - `releaseWorktree`'s JSDoc still said it returns "whether a live worktree was there to remove"; after the rmSync fallback it also returns true for an untracked leftover it clears, and a true return now means the path is gone. Reworded to match. Also noted at the probe pre-sweep why it keeps `releaseWorktree`'s two-step inline rather than calling it: the probe must target this repo via `cwd: worktree`, and it keeps the sweep's stderr for the create-failure detail. No behaviour change on the happy path (full review suite green, 417). * test(review): pin the probe create-failure detail with a pure helper Closes the last open thread on #6841: the `sweepErr` diagnostic had no coverage. The branch it lives on fires only when `git worktree add` fails, and there is no portable way to force that in a real-git test. The one lever — making `.git/worktrees` unwritable — is bypassed by root and behaves differently under CI's unprivileged user, so a test built on it would assert one thing locally and another in CI, which is worse than no test. So extract the part that actually has logic in it. `probeCreateFailureDetail` is pure: it names the `add` failure and folds in the stale-sweep's stderr, which is usually what explains it. Unit tests pin the fold, the omission of an empty sweep clause (a dangling "(stale-tree sweep also reported: )" would be report noise), and a non-Error throw. What stays untested is only the try/catch wiring. * fix(review): keep releaseWorktree non-throwing, and keep the discard's reason Three findings on the last round, two of them regressions I introduced. - **`releaseWorktree` must not throw.** The `rmSync` I added gave it a throw path it never had: `force` suppresses ENOENT but not EPERM or EBUSY, and this runs on the cleanup path, where an exception masks the error that got us there — which is exactly what its `gitOpt`-only body was avoiding, and what its "does not throw when git itself fails" test pins. The remove is now swallowed like every other failure here, and the outcome is reported through the return value instead: `true` now means the path is GONE (`existed && !existsSync`), not merely unregistered, so a caller can print "Removed …" without lying. - **The discard lost its reason.** Keying `cleanupFailure` off "does the path still exist" was right, but it dropped the `: ${e.message}` the old code carried. A bare "could not remove <path>" tells whoever has to delete the tree by hand nothing about why they must. Restored, preferring the `rmSync` exception and falling back to what git said when it refused to unregister. - **The two-step was duplicated within the file.** Pre-sweep and post-run discard now share `discardWorktree()`, whose doc carries the reason it is not a call to `releaseWorktree` (it needs `cwd: worktree` and the sweep's stderr). `probeCleanupFailureDetail` is extracted and unit-tested for the same reason its sibling `probeCreateFailureDetail` was: the branch cannot be forced portably, but the composition is where the logic is. Full review suite green (423). * fix(review): stop releaseWorktree losing the reason a path survived Two findings on the last round, and the first is a regression I shipped. **A leftover we could not delete went silent.** Making `releaseWorktree` return "the path is free now" stopped it lying — cleanup no longer prints "Removed …" over a directory that is still on disk — but it swapped one failure for another: `cleanup.ts` only prints on `true`, so a path it could NOT free now produces no output at all. The leftover survives, wedges the next `git worktree add`, and nobody is told. A boolean cannot say both "there is still something there" and "here is why", and the caller needs both. So it returns a result: `existed`, `freed`, and `reason` when it is still there (the `rmSync` errno if there was one — EPERM/EBUSY, which `force` does not suppress — otherwise a plain statement of the situation). `cleanup.ts` now reports in both directions: "Removed …" only when the path is actually gone, and "Failed to remove … : <reason>" on stderr when it is not. **And the failure branch is now tested.** It needs `rmSync` to hit EPERM/EBUSY, which nothing portable forces: root bypasses the permission lever, CI's unprivileged user behaves differently, and a `node:fs` module mock does not reach this module under the suite's config (verified — the mock applies to the fs namespace but `git.ts`'s binding stays real). So the ruling is a pure function, `worktreeReleaseResult`, tested for all five shapes — same treatment the two probe-detail helpers got, for the same reason. Full review suite green (428). * fix(review): un-strand releaseWorktree's doc; stop "Nothing to clean" contradicting stderr Two from review, both mine. - Extracting `worktreeReleaseResult` left `releaseWorktree`'s doc comment stranded: the block describing it (the `prune`-before-`branch -D` deadlock, the registered-but-missing worktree) ended up above the new pure helper, and `releaseWorktree` itself was left undocumented. Moved back where it belongs. - `cleanup.ts` treated "failed to remove" as if nothing had happened: the stderr line went out, but `removedAny` stayed false, so the run went on to announce `Nothing to clean for target "…"` on stdout — the two streams contradicting each other, and the stdout half being the one a script reads. "Nothing to clean" is a claim about the tree, not about this run's luck: it is only true when there was nothing there, not when there was and we could not get rid of it. Tracked with a separate `failedAny`, which also closes the same latent contradiction on the temp-file removal path, where it predates this PR. |
||
|
|
fea3ab3854
|
feat(serve): add extension management v2 (#6825)
* feat(cli): workspace-qualified extensions REST (daemon multi-workspace) Mirror the daemon extension-management REST surface to per-workspace routes, reusing the Phase 3 runtime resolver and trust gate. Extract a per-workspace extensions controller so the primary workspace shares one install queue, operation history, and status cache across the legacy and workspace-qualified routes. Reads resolve the target runtime only; mutations require a trusted workspace. Advertise a new baseline capability so clients can discover the surface, and add matching SDK client methods. Refs #6378. * qwen: address PR review feedback (#6638) Align the new extensions controller file's copyright year with the other new files added in this change. * qwen: address PR review feedback (#6638) Redact credentials from the extension source on the two success-path fan-outs (session refresh and refresh-failure broadcast), matching the operation record and failure broadcast. Document the non-cancellation semantics of the extension timeout wrapper. * qwen: address PR review feedback (#6638) Share the queue-full sentinel message via an exported constant so the throw site (controller) and the 429 match site (routes) cannot drift after the module split. Include the bound workspace in the extension operation log prefixes so concurrent per-workspace controllers are distinguishable in stderr. * feat(cli): add concurrent extension preparation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): remove redundant extension context build Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): address extension review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address final review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address latest review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): reject links in npm extension archives Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): limit npm extension archive downloads Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address review follow-ups Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address latest review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): release rejected operation slots Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address operation review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): align archive handling contracts Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): preserve watcher generation state Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(extensions): align management contracts Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk): bound extension operation polls Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(core): cover forged prepared commits Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(core): assert activation generation increment Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): close archive and polling gaps Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): retry suppressed extension generations Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): cover archive URL extension updates Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk): preserve unbounded operation waits Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): share npm redirect download deadline Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): preserve extension reload diagnostics Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): preserve installed Claude plugin paths Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): return committed activation state Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): preserve extension preparation errors Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): validate extension setting env vars Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): target extension reconciliation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): cover resultless legacy commit warnings Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): retain suppressed extension generations Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): record legacy runtime reconciliation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): validate extension clients by runtime Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): record workspace activation refresh Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): stop extension reconcilers after cases Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): resolve global runtimes at reconciliation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): reconcile newly registered runtimes Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): prevent overlapping runtime reconciliation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): dispose late runtime apps during shutdown Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): keep projection repair best effort Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): preserve committed store results Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): quarantine corrupt store journals Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): harden npm download redirects Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address review edge cases Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): honor cancellation between preparation stages Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): retry prepared cleanup failures Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(extensions): cover committed artifact recovery boundary Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): release extension refresh queue on timeout Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix: address extension review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6638) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): reconcile extension store compatibility state Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): bound npm redirects and isolate extension tests Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): make extension uninstall store-authoritative Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): defer prepared extension secret mutations Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): validate staged extensions before commit Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): enforce public extension network policy Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): handle extension response failures Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): surface committed refresh warnings Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): guard timer unref calls Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): release commit lane after durable writes Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(serve): update mutation callback assertions Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): refresh live extension instructions Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address latest review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address follow-up review findings Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address remaining activation feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): preserve preparation queue status Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): enforce network request deadlines Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(serve): clarify single-workspace capabilities Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): guard deferred settings commit Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): cancel archive extraction Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): harden refresh recovery Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(serve): serialize extension reconciliation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(extensions): address post-commit review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: fix CI failure on PR #6825 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: fix CI failure on PR #6825 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix: address critical PR review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): bound legacy extension update checks Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(acp): deduplicate extension refresh requests Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6825) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk): update browser bundle budget after main merge Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
f3358353d4
|
fix(review): build the chunk agent's prompt in code — they were launched blind (#6840)
* fix(review): build the chunk agent's prompt in code — they were launched blind
The review agents did not whiff. They were never given the diff.
Measured against the harness's own record of what they were actually started with
— the first record of each subagent transcript, written at launch and not
retconnable — 23 of 23 chunk agents got a prompt that named no diff file at all:
no path, no `read_file`, no offset, no limit. All 23 made zero tool calls.
What they got was a *description* of a chunk they had no way to open ("The changes
are in chunk 13 of 23, covering lines 3808-4024 of the diff"), and a sentence to
say if they found nothing ("If you find no issues, say `No issues found —
reviewed chunk 13 (…)`"). Every one of them said it. The receipts that looked
like proof of work were in the prompt that launched them; the coverage machinery
downstream was reading them back.
The skill has always specified what that prompt must contain — the diff path, the
agent's own offset and limit, the paging rule, the uncoverable rule. It said so
in a paragraph, and the orchestrator composed the prompt itself. This is the same
failure the skill has now fixed five times: a rule stated in prose is a rule that
will eventually not be followed, and the fix is to move it into code that can say
no. It was applied to the review target, the posting gate, the verdict and the
coverage report. The agent's own prompt — the thing that decides whether a review
can happen at all — was the one place it was not.
`qwen review agent-prompt --plan <report> --chunk <id>` now builds it. The
orchestrator passes what it prints, verbatim. A plan with no `diffPathAbsolute`
is an error rather than a prompt that merely describes the chunk, because that
prompt is the bug.
The generated prompt deliberately does not hand the agent a stock sentence to
recite when it finds nothing — it asks the agent to name what it examined. A
return that names nothing it read is indistinguishable from never having read
anything, and an agent that cannot open the diff will recite the sentence anyway.
* fix(review): stop asking an unreachable chunk for two contradictory things
A chunk holding a single line longer than one read is told to return
`Uncoverable: chunk N` — and was then also told, unconditionally, to end with
`Covered: chunk N lines X-Y`. Two instructions that contradict each other, and a
chunk that reports itself both uncoverable and covered is neither. The receipt is
no longer appended to a prompt that already asked for the honest answer.
Also: the read cap is the shared `READ_FILE_CHAR_CAP` rather than a second copy
of 25 000 that would silently diverge from it, and a plan that cannot be read or
parsed now names itself instead of surfacing a raw stack trace.
Three test gaps closed: the receipt line a normal chunk must carry (the structured
line `check-coverage` parses, previously asserted nowhere), the empty-`files`
fallback, and the command handler itself.
* fix(review): pass the project rules to the agent that is meant to enforce them
`buildChunkAgentPrompt` took a `rules` argument and had a test for it, and the
CLI had no flag to supply one — so `runAgentPrompt` never passed it. The rules
Step 2 loads were read, written to a file, and silently dropped.
That was survivable while the orchestrator assembled the prompt itself and could
staple them on. It is not survivable now: the skill says this command builds the
prompt and to pass what it prints verbatim, so there is no later step in which the
rules would arrive. The review would enforce no project rule at all, and say
nothing about it — a silent hole exactly where the skill promises a check.
`--rules <path>` now reads them in, and the skill passes it whenever Step 2 found
any. A path that does not resolve is an error rather than a review that quietly
proceeds without the rules it was told to enforce.
Also: the command-boundary test read `mock.calls[0]` with no `mockClear` between
tests, which was right only for as long as no earlier test invoked the handler.
* fix(review): guard the plan's files[] elements, not just the array
The plan is parsed off disk with an unchecked cast. A malformed entry rendered as
`- undefined (new-side lines undefined-undefined)`, sending the agent looking for
a file that does not exist. Bad entries are dropped; good ones still render.
|
||
|
|
53468cd8af
|
feat(daemon): add workspace skill toggle API (#6816)
* feat(daemon): add workspace skill toggle API * test(daemon): cover skill toggle capability integration * fix(daemon): harden skill refresh handling * fix(daemon): improve skill refresh diagnostics * test(daemon): expand skill toggle coverage --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> |
||
|
|
d1cda89875
|
feat(serve): add model API error & retry metrics to daemon status (#6837)
* feat(serve): chart model API errors and auto-retries in daemon status The Daemon Status dialog's Metrics tab tracked client→daemon HTTP request errors but had no visibility into model/LLM API failures or the automatic retries that absorb them. Add a "Model API health" chart plotting the per-window model API error and auto-retry counts. Those events originate in the ACP child (where LLM calls run), so they reach the daemon over the same child→daemon channel token usage already rides: a self-contained process-global counter (api-activity-tracker) is bumped at the logApiError / logApiRetry choke points, drained per live model round by the ACP MessageEmitter onto agent_message_chunk._meta, sniffed at the bridge fan-in, and folded into the daemon metrics ring as the llmApiErrors / llmApiRetries window aggregates (mirrored into the SDK wire type). A dedicated counter — rather than a new SessionMetrics field — keeps the change from rippling through ~20 stats/display/snapshot sites. Draining is guarded on a live frame's durationMs so replayed history can't consume real pending counts. * feat(web-shell): add per-chart help tooltips to daemon status metrics The Metrics tab packs ~12 dense time-series charts; with no inline explanation they generate recurring "what does this measure / why are there two 'errors' charts?" questions. Add an ⓘ affordance beside every chart title whose hover/focus reveals a one-line, plain-language note (what it measures, its unit/window, and what's normal). The button's aria-label carries the text to assistive tech; the visual bubble reuses the existing data-tooltip's surface so it reads in light and dark. The copy targets the highest-friction ambiguities: the HTTP "Requests" chart is called out as NOT model calls (vs. the model "API health" chart), and "API health" spells out the errors-vs-retries relationship (each failed attempt = 1 error, each backoff = 1 retry; errors above retries = hard failures). en + zh. * fix(web-shell): keep chart ⓘ hint out of the heading accessible name Review follow-up on #6837: - Move InfoHint out of the <h3> into a flex card header. The accessible-name algorithm folds a descendant button's aria-label (the whole help sentence) into the heading name, so a screen reader would announce "Model API health Each failed attempt = 1 error…" in the heading rotor. The flex header keeps the visual layout identical; a test now asserts no <h3> contains a button. - Add the missing logApiError OTel-SDK-off test, mirroring the logApiRetry one, so a future refactor that moves recordError() after the isTelemetrySdkInitialized guard is caught by a red test. * fix(web-shell): visible focus on chart ⓘ button, plus review nits Second review pass on #6837: - InfoHint focus: the ⓘ button now takes a filled-background focus/hover state (--muted, in scope under the DialogShell portal), matching the dialog's own header buttons (DialogShell .close / .iconButton). Keyboard focus is now unmistakable rather than a color shift alone (WCAG 2.4.7). - logApiRetry: doc comment corrected from "three-sink" to a four-sink fan-out — the apiActivityTracker increment (sink 0, fires before the SDK guard) was undocumented and read as stale. - Tighten the Metrics-tab SvgLineChart count assertion from >=10 to >=12 so a silently dropped chart card fails the test. |
||
|
|
fc43e8cad6
|
fix(cli): escape < in insight report data to prevent script breakout (#6802)
* fix(cli): escape < in insight report data to prevent script breakout renderInsightHTML embedded JSON.stringify(insights) raw inside an inline <script>. insights carries user chat summaries, file/tool names, and LLM output, so a </script> substring in any of them closed the tag early — breaking the report page or allowing script injection into the locally-opened HTML. Escape < to \u003c (valid JSON, parses back to <), which neutralizes </script>, <script, and <!-- while leaving the parsed data unchanged. * fix(cli): also escape U+2028/U+2029 in insight report data These line/paragraph separators are emitted raw by JSON.stringify but are line terminators to pre-ES2019 engines (embedded WebViews, older Electron), where they would throw a SyntaxError in the inline script. Escape them alongside the existing ` < ` escape; both round-trip back to the original characters. Adds a regression test. |
||
|
|
b155983db1
|
fix(cli): bound LlmRewriter outputHistory to contextTurns (#6799)
outputHistory pushed every rewrite but only the last contextTurns entries are ever read, so it grew unboundedly across a session (worst with contextTurns=0, where it accumulates yet is never read). Store nothing when contextTurns is 0, keep everything for 'all' (Infinity), and otherwise trim to the last contextTurns after each push. |
||
|
|
369789e67b
|
fix(cli): drain rewrites enqueued during waitForPendingRewrites (#6800)
waitForPendingRewrites snapshotted pendingRewrites via Promise.allSettled then reassigned the field to []. A flushTurn that landed during the await appended to the array and was discarded by the reassignment, so its rewrite was no longer awaited before session end. Drain in a loop instead: take the current batch, clear the queue so concurrent pushes go into a fresh array, await it, and repeat until empty. |
||
|
|
ff91207502
|
fix(cli): compute latestActiveTime from the real activity timestamp (#6834)
latestActiveTime was derived by iterating the date-only heatmap keys, so new Date(key) was UTC midnight and toLocaleTimeString() always returned a constant (the timezone offset expressed as a time) instead of the real last-active time. Track the most recent user-interaction timestamp in the metrics loop and format that instead. |
||
|
|
220fba7917
|
feat(subagents): make Explore inherit the main model by default (#6807) | ||
|
|
9dd8389ebe
|
fix(serve): Route session continue, language, and artifacts by owner (#6833)
* fix(serve): route session mutations by owner Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6833) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
36cf31fb6c
|
refactor(review): run the test-efficacy probe in a disposable worktree (#6836)
Some checks are pending
The probe used to revert the PR's source to base IN the shared review worktree and restore it in a `finally`. That shared tree is the one every Step 3 review agent reads, and the in-place mutate/restore was the root of two findings on #6790: - a concurrent reader could observe the tree half-reverted to base for the probe's whole duration (Critical), and the later restore cannot un-produce a finding written from the wrong source; - the restore's in-place delete followed a PR-controlled symlink out of the tree and removed an outside file (P0, band-aided with `safeRmWithin`). Both share one cause — mutating a live, shared tree — and one fix retires both. The probe now runs in its OWN disposable worktree, checked out at the PR head as a sibling of the shared one (`.qwen/tmp/review-pr-<n>-probe`) and removed wholesale when it finishes: - the shared tree is never touched, so no reader can see a reverted state; - there is no in-place restore, so the delete that followed a symlink is gone with it — `safeRmWithin` stays only as belt-and-suspenders on the revert-phase delete of added files; - `node_modules` needs no per-tree install: the probe tree is nested under the repo, so `npx vitest` resolves upward to the repo-root `node_modules`, exactly as the shared worktree already does. (Confirmed empirically before relying on it — this is what had the refactor deferred.) Because the shared tree is no longer mutated, the dirty-worktree guard is gone (nothing the caller has uncommitted is ever discarded), and the loud `restoreFailure` / non-zero exit becomes a soft `cleanupFailure` warning: a leftover probe worktree does not corrupt anything and is swept at the next run's `worktree add` and by `cleanup.ts`. Verified by driving the real handler (new `test-efficacy.integration.test.ts`, real git worktrees, a stub vitest bin): verdicts are unchanged (gated/inert), the shared tree is byte-identical before and after, the probe tree is always discarded, and the symlink P0 repro leaves the outside file intact WITHOUT `safeRmWithin` having to refuse — isolation alone protects it. Closes #6832. |
||
|
|
a1806e404f
|
feat(review): capture untracked files, resolve anchors from snippets, and gate posting in code (#6771)
* feat(review): capture untracked files and resolve comment anchors from code snippets
A local `/review` captured its diff with `git diff HEAD`, which reports changes
to files git already tracks. A file the user created and has not staged is in
neither the index nor HEAD, so it appeared in no diff at all: brand-new files
went entirely unreviewed, and a working tree whose only change was a new file
reported "no changes to review" and stopped. Applying a real 1024-line pull
request to a working tree without staging it reproduces this — the old capture
sees zero bytes.
The capture now diffs each untracked, non-ignored file against the null device
with `--no-index` and appends the section. It deliberately does not stage them:
`git add -N` would make them visible to `git diff` by writing to the user's
index, and reviewing code must not modify the repository it is reviewing.
Oversized and unreadable untracked files are named rather than dropped in
silence, since a quietly skipped file is the bug being fixed here. The capture
lives in a subcommand alongside the PR one, so both pin the same diff flags from
one constant instead of drifting apart.
Review findings also carried a line number the agent derived by counting hunk
headers, and that number was posted straight to GitHub. GitHub rejects an entire
review with a 422 when any one comment's line falls outside every hunk, so a
single miscount took every blocking finding down with it, and the recovery path
then discarded the unanchorable finding outright. Findings that were right about
the code were being thrown away over arithmetic.
Each finding now quotes the code it is about, and the line is computed from the
diff by consecutive-line matching, with the agent's claim kept only to break a
tie when the snippet genuinely repeats. Because every candidate line is
collected from inside a hunk, a resolved anchor is a valid anchor by
construction. A quoted snippet also resolves a multi-line range, which a bare
line number cannot express, so a comment can highlight a whole construct instead
of its last line.
Measured on two real pull requests, agents in fact counted well: 21 of 22 line
numbers were exactly right and none would have failed. The anchor is not here
because counting usually fails — it is here because when it fails it fails
silently and takes the whole review with it, and because a derived number is
better evidence than an asserted one.
* fix(review): never report an undiffable untracked entry as reviewed
`git ls-files --others` does not only name regular files. An embedded git
repository comes out as a directory, and so does a symlink to one. Both passed
the size gate, because stat follows links and succeeds on a directory. `git diff
--no-index` then failed on them the way it signals every difference — by exiting
1 — but with empty output, and an empty buffer is a truthy object, so the exit-1
tolerance accepted it as a diff of nothing. The path was recorded among the
files whose contents are in the diff. It contributed none, and it never reached
the skipped list.
That is the invariant this feature exists to protect, inverted: the capture
claimed to have reviewed something nobody had looked at, which is worse than the
bug it replaced — that one at least never claimed otherwise. Embedded repos in a
working tree are ordinary; a scratch clone or a vendored checkout is enough.
Entries are now typed before they are handed to git, following a symlink first
because git decides what an argument is from its resolved type too, and anything
that still fails to render is recorded as unreviewed rather than as reviewed. A
dangling symlink is deliberately not skipped: git renders the link text, and a
link pointing nowhere is worth a reviewer's attention.
The untracked pass also had no aggregate ceiling. Each file costs one git spawn,
and a working tree whose ignore rules do not yet cover a dependency directory
offers tens of thousands of them, so the fix for "shows nothing" could become
"hangs for minutes". A file count far above any real change means the ignore
rules are broken rather than that a lot of code was written, so the pass is now
abandoned wholesale in that case — checked before the loop, at zero spawns — and
a running total bounds the rest. Both report through the same channel as every
other skip.
Two smaller things. An anchor snippet copied straight out of a diff, markers and
all, is a mixed run of added, context and removed lines; that is not the
all-added form the marker-stripped reading expected, so it matched nothing. The
new side of such a region is exactly its added and context lines with the marker
column removed, which is an exact reconstruction rather than a guess, and it now
resolves. And duplicate finding ids are refused: resolutions are matched back to
findings by id, so a duplicate would pair a finding with another one's line and
post a comment on code it is not about.
* fix(review): stop certifying unreadable files, and refuse to guess an anchor
A binary file is the directory bug an octave quieter. Git renders one as a
single `Binary files ... differ` marker; the section is well-formed, parses, and
contains not one byte a reviewer could read — yet its path was recorded among
the files whose contents are in the diff. A small PNG or a stray `.pyc` was
certified as reviewed by a review that could not see it. Binary sections now go
to the skip list with the rest of the unreadable scope.
Two things the repo-root switch broke for a scoped review. The path a user types
is relative to where they typed it, and every git call runs from the repository
root, so asking for a file from a subdirectory searched a different file and
reported no changes. And `--` ends option parsing without disabling pathspec
magic: a filename is not a pathspec, so scoping a review to `a[bc].ts` also
dragged in `ab.ts` and `ac.ts` — a review that says it looked at one file and
looked at three. Paths are now rebased against the caller's directory, bounded
to the repository, and read literally.
The empty tree is asked of git through an empty stdin rather than by naming the
null device. Git's special case for that name lives on the diff code path only,
so hashing an object would have tried to open it as an ordinary file on Windows.
A capture that skipped everything it found is not a clean tree, and no longer
says it is: an oversized blob or an embedded repository as the only change used
to produce an empty diff and the word "clean", which is a green verdict over work
the capture explicitly could not read. Filenames are also escaped for display —
git permits newlines and escape sequences in them, and they were being printed to
a terminal verbatim.
The anchor resolver stops guessing. It searched added lines and context lines in
separate passes and returned on the first hit, so a snippet that also sat on a
context line elsewhere came back as a single unambiguous match — a tie it never
saw rather than a tie it broke, and the wrong line when the finding meant the
other copy. Candidates are now collected across both at once. With nothing to
choose between them — no claimed line, or several indentation-stripped candidates
in a language where indentation is the semantics — it refuses rather than picking
one: an unmatched finding is loud and recoverable, a blocker posted on the wrong
one of two identical lines is neither. Loose matching is offered only to a
verbatim snippet, never stacked on top of a marker-stripping guess.
Multi-line comments need both side fields, and GitHub rejects the whole review
without them. Pattern-aggregated findings carry one anchor per location, since
one anchorless entry would abort the resolution of every other finding in the
batch. Test fixtures no longer inherit the developer's git configuration, and the
hostile-config fixture is driven by the production flag constants instead of a
private copy that could not notice them changing.
* fix(review): make the run's own bookkeeping computed, not recalled
Dogfooding the skill against the pull request that changes it surfaced four
defects. All four are the same shape: a rule the prompt states as a thing to
remember, which a model under load simply does not.
A bare PR number does not say which repository it belongs to, and the skill
never said where to get that. It explained the derivation for a URL target and
left the number target to improvise, so in a fork clone — the maintainer's own
setup — the model inferred the fork from the branch's push target, the fetch
answered "could not resolve to a PullRequest", and the review stopped before
reading a line of code. The owner and repo now come from the same query the
posting step already uses, and the remote is the one whose URL matches it rather
than whichever is called origin: in a fork clone origin is the fork, which
carries no ref for an upstream pull request.
The final machine-readable line announced a posted approval on a run that posted
nothing. The posting gate had correctly blocked every write — no flag, no
request — and the line still said so, which is the one place a batch driver or a
log scraper looks to learn what happened. The word is now a fact about whether
the submission call was made, stated as such: the gate and the line are the same
fact twice, and they cannot disagree.
The substantive-return check is performed silently, so it is skipped. The issue
agent returned in six seconds having made a single tool call — the textbook
whiff the check exists to catch — and the review went on to report full coverage
and approve. It is now a roll-call written down before verification begins: one
line per agent naming the artifact its return actually named. A line cannot be
written for an agent that named nothing, and a whiff on the page is a whiff that
must be acted on.
And a review of a good change is allowed to be empty. That run padded its output
with five "suggestions", each summarising something the diff already did, each
with a suggested fix of "already implemented". The exclusion criteria now say so:
a description of what the change does is a changelog entry, and a reader who has
to work through five of them to discover there was nothing to do has been given
noise wearing silence's clothes.
* fix(review): put the posting gate behind code that can say no
Reviewing this pull request with the skill it changes, twice, produced the
failure the gate exists to prevent. The second run was given a bare `/review
6771` — no flag asking for comments, no request to publish — and it filed a
public review on the pull request anyway, announcing inline suggestions it had
not posted, with the footer's newlines surviving into the text as literal
escapes. Nothing stopped it, because nothing was there to stop it: the gate was
a paragraph asking the model to check before writing, and a paragraph is not a
gate. It had already failed once before, and the prompt says so.
The model did not defy the rule either time. It reasoned its way to a verdict it
wanted to file and never re-read the sentence forbidding the filing. That is the
same failure the review event and body had, for the same reason, and the skill
already knows the fix — those were reasoned about at submit time, got it wrong
five times running, and became a subcommand that computes them. Whether to write
at all is the same kind of decision, and the authorisation was already a computed
fact: the argument parser has emitted it since the first step.
So the write moves behind it. There is now one command in this skill that talks
to the pull request, it is shown the parser's verdict, and it refuses when that
verdict does not authorise a post — or when the user has not asked for one in so
many words. A refusal is a complete outcome, not an error to route around: the
findings are in the terminal and the saved report, and the user is invited to
publish them if they want to.
It also refuses a payload that argues with itself before GitHub can accept it —
GitHub takes all of these, and the author is the one who finds out. A body
promising inline comments beside an empty comment array. A literal escape
sequence from building the request with shell interpolation instead of a file. A
multi-line range missing the side fields that GitHub requires and rejects the
whole review without.
The prompt keeps the gate's text, because a reader still needs to know what
authorises a post. It no longer pretends that text is what enforces one.
* fix(review): write the report where the caller asked for it
Two of the new subcommands created their own temp directory and then wrote to a
path the caller chose, which is not necessarily inside it. `--out
reports/plan.json` is a legal request, and both answered it with ENOENT. The
sibling that composes the review has had this right all along; these two did not
copy it. Each now creates the directory of the file it is about to write —
capture-local creates both, because its diff genuinely does belong in the temp
directory while its report belongs wherever it was asked to go.
The submit check for a shell-escaped body was searching for a backslash-n
anywhere in it. A review body legitimately carries finding text, and finding text
quotes code — a regex, an escaped string — so the check could fire on a real
blocker, and a false positive here does not warn, it refuses the post and loses
the review. It now matches the bug's actual fingerprint: an escaped newline
immediately before the footer, which is what a body built by shell interpolation
looks like and what nothing else does.
Smaller things the same review surfaced. A malformed repository argument went
straight into an API path and failed as a confusing 404 from a URL nobody meant
to build; it is now refused. A JSON syntax error in the findings file reported
that the file could not be read, sending the reader to look at permissions. The
anchor batch is keyed by path and every test had used a single-file diff, so the
routing itself was never exercised against a second file — a bug leaking lines
between files would have passed. And the capture command's own boundary — its
report assembly, its skipped-file disclosures, its clean-tree branch, its
control-character escaping — had no tests at all, while the library beneath it
was covered thoroughly.
Four documentation defects, each one a place where the prompt describes an
earlier version of this change: the resolver's start line is what makes a
multi-line comment multi-line, and the guidance never said to use it; an
ambiguous anchor can also be settled by being the only candidate on an added
line, not just by a claimed line; the recovery path still asserted that no
comment sets a side, which multi-line comments now must; and the resolver's input
example presented an optional field as required.
* fix(skills): give a skill its arguments instead of asking it to remember them
A slash command's arguments reach the model, appended to the skill's prompt. But
a skill that needs them as *data* — to hand to a parser, to a subcommand, to
anything deterministic — has had to ask the model to copy them into a file, and a
copy is a recall.
It recalls wrong. Running `/review 6771` against this branch, the model wrote
`--effort high` into the argument file: not the user's argument, but an example
lifted out of the skill's own documentation. The parser then did its job
perfectly on the input it was given — resolved a local review, found the working
tree clean, and reported "no changes to review". A request to review a pull
request became a no-op, and nothing anywhere raised an error. That is the one
shape a review must never have: silence that looks like success.
So the CLI writes the arguments down at launch, verbatim, before the model has
any say in them, and the skill reads that file. Both loaders do it, the name is
sanitised because it becomes a filename, and a failed write degrades to what
happened before rather than taking the invocation down.
The same review raised the weakness underneath the posting gate, and it was
right. The gate read `comment.effective` out of the parser's JSON output — a
document the caller writes. A run that wanted to post could write
`{"comment":{"effective":true}}`, point the gate at it, and walk through;
confirmed against the built CLI in one line. The gate now reads what the *user*
typed, from the file the CLI wrote before the model existed to the prompt, and
runs the same tested parser on it. Forging authorisation means forging the user's
own keystrokes, which is not something a careless run does by accident.
Verified by running the skill against its own pull request a fourth time. It
resolved the right target, found five blockers, and ended with "Request changes,
not posted" — the three preceding runs had lied about posting, posted without
being asked, and lost the pull request number entirely.
* fix(review): stop being most confident where it is most wrong
A review of this branch found twelve blockers and every one was real. Several
are in the fixes for the previous round.
The anchor resolver could return a wrong line with every signal saying it was
certain. Two added lines whose code reads `+value;` make the faithful reading of
that anchor ambiguous — and the resolver, unable to choose, fell through to the
marker-stripped reading, which matched an unrelated `value;` uniquely and
reported it as a single unambiguous match. It was at its most confident exactly
where it was most wrong. A stronger interpretation that cannot decide now
outranks a weaker one that is sure, and both stop rather than continue. The same
resolver silently preferred the earlier of two equidistant candidates: with
matches at ten and twelve and a claim of eleven, nothing distinguishes them, and
answering ten with a straight face attaches a blocker to whichever occurrence
happened to come first.
The environment isolation added last round did not isolate anything. The git
wrapper snapshotted the environment into a module constant at import, and an
importer cannot set a variable before an import — so the suite that exists to
prove a hostile developer configuration cannot reach the capture had been running
with the developer's configuration the whole time, and the fix for it was a
comment. The wrapper now reads the environment when git is run. The neighbouring
fixture had the same hole: its repository loaded developer templates and its
commit ran their hooks.
Binary detection was wrong in both directions. It read a four-kilobyte window on
the theory that a binary section is short — the *header* is short, the path in it
is not, and a long enough path pushes git's marker out of the window and
certifies unreadable bytes as reviewed. And it searched for "GIT binary patch" as
a substring, which is a sentence, and sentences appear in prose: a markdown file
containing that phrase was thrown away unread. Both markers are whole records at
the start of a line; match them there, over the whole section.
Containment was checked by asking whether the relative path starts with two dots.
A file called `..foo.ts` at the repository root does, and a scoped review refused
to look at an ordinary file on the grounds that it had escaped.
The posting gate authorised a target it never checked. `--comment` on pull
request 6771 would authorise a submission to 9999 in another repository — the
flag was a bearer token rather than permission for a thing. And the payload check
guarded against one 422 while waving through several others: an event GitHub does
not accept, a comment with no body, a line that is negative or fractional or not
a number at all, a range that ends before it begins. Its inline-promise check
also searched the body for the word "inline", which is finding text — a blocker
reading "the inline cache is stale" was refused.
Two contradictions in the prompt, both introduced by the previous round's own
fixes. The completion contract still says to report `posted` only after a direct
API call, which the step now forbids, so a successful submission would announce
itself as not posted. And the head-advance guidance said to re-fetch and
re-resolve, which relocates the anchors of findings about code nobody has read:
if the head moved, the review is of a commit that is no longer the pull request,
and the answer is to start again, not to re-aim.
* fix(review): prefer the candidate that touches the change
The anchor resolver asked whether a matched run was made *entirely* of new code.
A two-line anchor spanning a context line and the added line beneath it is not,
so it was filed as "context" — indistinguishable from a wholly unchanged
duplicate elsewhere in the file. Faced with the two, the resolver could not tell
which one the finding meant and gave up on both. What matters is which candidate
touches the diff, not which is made only of it.
A region copied verbatim from the end of a diff brings git's "no newline at end
of file" note with it. That note is metadata, not a line of the file, and it
carries no marker column — so it disqualified the whole region from the
hunk-region reading and an otherwise unique anchor came back unmatched. It is now
permitted, and dropped when the new side is reconstructed.
The repository validator accepted `../repo` and `owner/..`: dot segments are made
of legal characters and mean something else entirely once they reach a URL path.
The pull request number was taken on yargs' word that it was a number, which
includes zero, negatives, fractions and infinity — each of them a puzzling 404
from a URL nobody meant to build. A null element in the findings array crashed
with a type error that named neither the entry nor the field, while every other
malformed input got a message that named both. And the path to the arguments file
was written out by hand next to a comment promising it was kept in step with the
module that owns it; it is now imported from there, because a comment is not a
mechanism.
Four things the prompt said that were no longer true, three of them made untrue
by this branch's own fixes. Step 1 still described the parser's JSON verdict as
what authorises a post, which is exactly the document the gate now refuses to
trust. Pattern aggregation's display rule — show three locations and a count —
was being read as a data rule, so the anchors Step 7 needs to expand the
aggregate were being truncated away. The final copy-ready submission recipes
dropped the Enterprise host flag the canonical one carries. And the 422 recovery
checked a multi-line comment's two endpoints independently, which passes a range
spanning two hunks and a range that ends before it begins — both of which GitHub
rejects, after a round trip.
* fix(review): refuse to certify a diff nobody read
The review approved a pull request that no agent read.
Dogfooded against its own PR, the orchestrator launched twenty-five agents over
an eighteen-chunk, 4 925-line diff. Twenty-two came back in under two seconds
having made zero tool calls, returning about nineteen tokens each — the length of
the words "No issues found." They had not opened the diff. The three that did
work were the three whose jobs do not require reading it: the one that runs the
build, the one that queries the issue tracker, the one that greps for tests. The
run then reported zero findings, wrote "Not reviewed: none", and filed an
Approve.
The prompt had three defences against exactly this, and every one of them was
prose. Each chunk agent "MUST" end with a coverage receipt. The orchestrator
"MUST" check that every chunk carries exactly one. An agent that returns
near-instantly with almost no output "did not do its job". None of the three
happened, and nothing downstream could tell: the verdict composer already refuses
to approve past an unreviewed chunk, but it refuses on the strength of a list the
caller supplies, and a caller that skipped the check supplies an empty one —
which is precisely what a clean review looks like.
So the coverage is no longer asked for. It is shown. The agents' returns are
written down verbatim and handed to a subcommand, which reads the receipts out of
what they actually said, names the chunks nobody covered and the agents that
returned nothing, and exits non-zero. Its report goes to the composer, where the
gaps become caps and forbid an approval exactly as a hand-written entry would.
Omitting the report is itself a cap: a run that cannot show what it covered has
not shown that it covered anything.
The orchestrator can still lie — it copies the returns. But fabricating eighteen
receipts is an act, and every failure this skill has actually suffered has been an
omission.
The threshold for "returned nothing" was set by driving the command against the
real transcript, which is how it was caught flagging a Build & Test return that
named its commands and their outcomes. The prompt's own model answer for a clean
return runs to a hundred and eight characters; a check strict enough to reject
that fails closed on good work.
* fix(review): make the coverage gate a gate, and close the seams around it
The coverage check added last commit was itself reviewed, and it did not hold up.
The composer took the check's report but only read two of its fields, so an
absent report, a report saying `ok: false`, and a report carrying an uncoverable
chunk all still composed an APPROVE — the doc comment promised "omitting it is a
cap" and the code did not implement it. Every failed-coverage state now caps, and
`ok: false` caps on its own: a report that says the diff was not covered is the
strongest statement in the input and must not need an itemised list to be
believed. An absent report does not cap, because that is what a local review and
every non-review caller of the composer looks like; the skill's own guarantee is
kept where it belongs, by the command's non-zero exit stopping the run before the
composer.
The check demanded a receipt from every chunk, but the small-diff fan-out has no
receipts and must not — so it would have blocked every small review at Step 4
over chunks it believed nobody read. It now takes the topology. It also credited
a receipt for any chunk named anywhere in an untrusted return, so a chunk-1 agent
could receipt chunk 2, or quote a receipt out of the diff it was reviewing; the
receipt is now bound to the agent's own assignment. Its substantive-return check
ran only when no receipt was present, so a receipt plus "No issues found."
cleared coverage having read nothing. It knew only the returns that arrived, so a
lens that was never launched was invisible; it now takes the expected roster. And
an agent label copied from the diff went to the terminal raw, so diff-induced
text could forge a return and inject terminal control sequences; labels are
sanitised and bounded.
Around the gate: the anchor resolver computed a minimum with `Math.min(...spread)`,
which throws past ~200k candidates and takes the whole batch down — a loop now. A
long-path test built a 2.2 kB path that macOS rejects with ENAMETOOLONG, reddening
the CI leg it was added for; it exercises the parser directly instead. The
authorisation record moved from a fixed per-skill filename to one scoped by the
session id, which the model cannot choose or see and which a submit subprocess
inherits from the environment, so a caller-selectable `--skill-args` is honoured
only in tests; the file is written with O_NOFOLLOW and mode 0600, because a
symlink at its path was a write-through primitive and arguments can carry a token.
And one of the new tests asserted the buggy behaviour it was meant to forbid — it
expected a cross-chunk receipt to be credited. Caught by mutation: the mutant that
removed the binding survived, because the test's own expectation matched it.
* fix(review): reconcile the session-scoped args file, and close the coverage seams
The session-scoped authorization file introduced last round moved the filename
without moving the four places the prompt names it — so a normal session wrote
`qwen-skill-args-<session>-review.txt` while Step 1, submit, and cleanup all read
`qwen-skill-args-review.txt`, found nothing, and fell back to model
transcription: exactly the hole the whole scheme exists to close. The scope now
lives in the directory, not the filename, so the file is always
`qwen-skill-args-review.txt` under a per-session directory, and the prompt reads
the exact path from the note rather than guessing a name.
The rest of that file had the seams a review found. The session id was read only
from the process-global environment, which in daemon mode belongs to whichever
session booted first; it now prefers the async-local context both the loader and
submit's shell environment already agree on. `writeSync` was trusted to write
every byte in one call and can write fewer, leaving a truncated record that
mis-targets the review; it loops now. And a bare invocation, which records no
arguments and so never truncated anything, left an earlier run's authorized
record in place for the later one to inherit — the bare path now erases it.
Two coverage-gate seams. A missing receipt capped the event but was left out of
the certification predicate, so a body could open "Reviewed — no blockers." two
lines above "nobody read them."; certification now requires every chunk read. And
`splitReturns` starts a record at any `=== AGENT: ===` line, including one quoted
inside an agent's own body — and the diff under review contains this file, whose
tests contain that header — so a diff-induced quote could forge an agent and a
matching receipt. Coverage is now credited only for a label on the caller's
expected roster; a forged off-roster label neither covers a chunk nor counts as a
whiff.
The retry recipe told the model to stop after a second whiff and also to never
proceed on a non-zero coverage exit — a deadlock. A twice-whiffed agent is now a
disclosed unreviewed dimension that caps the verdict at Comment, not an eternal
exit 3.
* fix(review): branch the coverage headline by topology
A dimension run has no receipts by design, so "0/N chunks receipted" read as a
failure it was not; it now says so plainly. And a receipt-bearing return that
said nothing was described as having "no receipt" — corrected to name what a
receipt does and does not prove.
* fix(review): type coverage.uncoverableChunks, and test it
The composer reads `coverage.uncoverableChunks` at runtime and folds it into the
uncoverable cap, but the field was missing from the `coverage` input type — so it
worked through JSON yet a TypeScript caller could not pass it, and no test
exercised the path. Declared and tested; an uncoverable chunk in the report now
demonstrably forbids an Approve.
* fix(review): make omitted coverage a cap, as the JSDoc always promised
The `coverage` input's doc said "Omitting it is itself a cap," and the code did
not do it: a caller that supplied no `coverage` at all skipped every coverage
cap and could still compose an APPROVE — the exact dogfood failure, an
orchestrator that skips check-coverage and gets a rubber stamp over an unread
diff. Last round I had narrowed the cap to a *present-but-failed* report to avoid
reworking the table tests, which left the doc promising more than the code did.
Absent coverage now caps, and a caller with genuinely no territory to cover says
so with `coverageNotApplicable: true` rather than by silence — because silence is
precisely the failure being guarded. The C/S-table and downgrade tests, which
exercise everything except coverage, opt out through the shared base; the PR
review path always supplies a real report and needs no opt-out. Two tests pin
the new behaviour: omitted-and-not-opted-out caps, opted-out approves.
* fix(review): remove the model-settable coverage opt-out I added last round
The `coverageNotApplicable` field, added one round ago so an absent coverage
report could still approve, came from the same model-authored JSON whose
omissions the gate exists to distrust. A review that wanted an approval could set
it and walk straight through — the unread-PR failure with one more keystroke,
reproduced as `composeReview({ modelId, coverageNotApplicable: true }) →
APPROVE`. It is gone. Absent coverage caps, full stop; a caller with genuinely no
territory passes a report that says so (`{ ok: true }`, empty lists), not a
boolean it authors. The table tests that reach a clean APPROVE now supply that
report, the same one the PR path passes.
(The report itself is still model-relayed — the deeper limit a review keeps
reaching, that no model-written field authorises itself. Closing it means reading
the harness's own per-agent transcripts, tracked separately. This change removes a
backdoor I opened; it does not claim to close that limit.)
Two seams in the args file. `O_NOFOLLOW` guarded the filename but not the parent:
a symlinked `s-<session>` directory redirected the write into an arbitrary file,
truncating it at mode 0644 and exposing the raw arguments — the write now refuses
a symlinked session directory. And `clearSkillArgs`, which revokes a prior run's
posting authority on a bare invocation, swallowed a removal failure and returned
as if revoked; it now returns whether the record is actually gone.
* fix(review): flatten target filenames, exclude own scratch, post validated bytes, cap the tracked diff
Four findings from a review of the local-capture and submit paths.
The `target` label was interpolated straight into a temp filename, so a
file-path review of `src/foo.ts` produced `qwen-review-src/foo.ts-diff.txt` —
a nested path whose parent nobody created (ENOENT) — and a crafted `../../evil`
escaped `.qwen/tmp` entirely, letting the write land anywhere. It is reduced to a
single safe filename component, with no separator or dot-run surviving.
The capture listed the review's own `.qwen/tmp` scratch files — the args record,
the parsed verdict, the diff, the plan, all written before it runs — as the
user's untracked work whenever the repo did not ignore `.qwen`, so the review
reported on its own plumbing. Those paths are dropped.
`submit` posted by handing `gh` the review file's pathname, which `gh` re-opens:
a swap or truncation between validation and the post would send GitHub bytes that
never passed the gate. It now sends the parsed, validated object over stdin, so
the bytes checked are the bytes posted.
The aggregate size budget covered only untracked files; a tracked diff could
still grow to the 512 MiB git buffer and then be concatenated, decoded and
re-split. A tracked diff over the whole-capture cap is now reported and left out,
the same as an oversized untracked file.
* fix(review): close the gaps the last two rounds of fixes opened
Three of these are mine, introduced by the fixes that preceded them.
`coverage: {}` composed an APPROVE. It fell between both guards: the report is
"provided", so the absent-coverage cap was skipped, and `Object.keys({}).length
> 0` was false, so the failed-coverage cap was skipped as well. A report with no
`ok: true` in it has not shown coverage, however few keys it has — the key count
is gone from the condition.
Removing `coverageNotApplicable` last round left the prompt still telling the
model to set it, so the skill documented a field the code rejects. Removed there
too.
`clearSkillArgs` was given a boolean return so a failed revocation could not pass
for a successful one — and both loaders then discarded it, which is precisely
what its own JSDoc forbids. A bare invocation whose revocation fails now tells
the skill the stale record survived and does not speak for this run.
Two more from the same review. `toNumberList` admitted `NaN`, `Infinity`,
negatives and fractions as chunk ids, any of which would render into the review
body as "chunk NaN"; it now requires a positive whole number, as its sibling
`toCount` always has. And `--file` was resolved without following symlinks while
git's repo root is canonical, so on macOS a `--file` under `/tmp` (a symlink to
`/private/tmp`) relativised against a root sharing no prefix and was rejected as
outside the repository; both sides are canonicalised now, falling back gracefully
for a file that does not exist yet.
|
||
|
|
536cb713c6
|
fix(ui): refine reasoning duration displays (#6793) | ||
|
|
7129a43c54
|
test(cli): remove flaky headless child-process recording test (#6830)
The 'keeps a headless child alive until a never-resolving flush times out' test spawns a real child process with --import tsx and a 5 000 ms execFile timeout. The settle timeout alone is 2 000 ms, so tsx cold-start + module load + the 2 s timer leave almost no headroom. On GitHub-hosted runners the total regularly exceeds 5 s, the child is SIGTERM'd, and the test fails with "Command failed" even though the underlying logic is correct. The sibling test 'stops waiting after two seconds without cancelling the write' already covers the same timeout branch with fake timers, making the real-process test redundant signal at high flake cost. Remove the flaky case and the now-unused execFile / path / promisify imports. |
||
|
|
048ced7d0e
|
fix(channels): distinguish slash-command output (#6818)
* fix(channels): distinguish slash-command output * fix(channels): deliver slash command output in block streams * fix(channels): isolate daemon session updates * fix(acp): isolate slash command rewrite metadata * test(channels): cover slash command review cases * test(acp): assert slash command rewrite isolation |
||
|
|
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. |