Commit graph

661 commits

Author SHA1 Message Date
易良
fc874dfe0b
fix(ci): retry sandbox image builds and file an issue when a release build fails (#9916)
* fix(ci): retry sandbox image builds and file an issue when a release build fails

The v0.22.0 tag build died on a transient ETXTBSY during `npm ci` and was
never retried, so ghcr.io/qwenlm/qwen-code:0.22.0 was never published while
npm already served 0.22.0. Every sandbox-based CI lane (/resolve, sandboxed
review, autofix) then crashes with "manifest unknown" until the image exists.

Add one bounded retry to the buildx step: the first attempt carries
continue-on-error so a successful retry turns the job green, and the retry
gates on the first attempt's outcome alone (a failure() gate would read false
once continue-on-error absorbs the first attempt). The publish condition is
shared through one job-level env so the two build steps cannot drift.

Add a follow-up job that files or updates one issue per version when both
attempts fail — for tag pushes and for publishing dispatches alike, since the
issue body itself recommends that dispatch as the recovery path. Dedup uses
an exact body marker matched client-side, because GitHub search tokenizes the
colon out of the marker and never finds these issues.

Extend the existing workflow gate test to pin the retry contract and the
issue-job gate.

Fixes #9898

* fix(ci): move the image-build failure issue logic to .github/scripts/

The workflow-size ratchet rejects growth past the recorded baseline +4096
bytes; the inline issue-filing step grew build-and-publish-image.yml by
~5.2 KB. Move the step body to .github/scripts/image-build-failure-issue.sh
(the gate's own recommended remedy), leaving the job as a thin env + script
call. No behavior change; the gate test now pins the script call and reads
the dedup contract from the script.

* fix(ci): grant the failure-issue job contents permission and normalize dispatch versions

* test(ci): pin the failure-issue gate and retry step invariants

* fix(ci): gate the failure-issue job on the exported publish decision

* fix(ci): skip the failure-issue job for versionless publishing dispatches

* test(ci): pin the PUSH_IMAGE value and the login gate at the definition site

* test(ci): replay the image-build failure-issue script under a gh stub

* fix(ci): describe release build job failures without asserting a buildx cause

* fix(ci): preserve annotations and recorded runs when updating the failure issue

* test(ci): pin the dedup label on create and the open-state filter on lookup

* fix(ci): document the pre-first-step gap in the failure-issue gate

A build job that fails before its first step runs (runner provisioning
failure) never executes publish-decision, so push_image stays empty and
file-failure-issue is skipped despite failure() being true. Closing the
gap structurally would restate the publish predicate and re-introduce
the drift this PR removes, so document it on the job comment instead:
future "failed publish, no issue filed" investigations start here, and
a scheduled npm-vs-GHCR reconciliation remains the backstop.

* fix(ci): record build-and-publish-image.yml's shipped size in the workflow size baseline

The retry logic and failure-issue filing steps added by this PR grew the
workflow from 4638 to 8887 bytes, past the 4096-byte allowance. Record
the new size so the size gate passes.

* fix(ci): harden the image-build failure reporter per review round 4 (#9916)

- Replace GNU-only `head -n -1` with POSIX `sed '$d'` so the stranded-heading
  strip no longer corrupts the body on BSD userland (R4-1).
- Skip the bash replay suite on win32, where backslash RUNNER_TEMP and the
  ';'-separated PATH cannot express it; the YAML pins still run there (R4-2).
- Re-check head readability AFTER the normalization strip, which can itself
  empty the head and used to drop the narrative permanently (R4-8).
- Admit only recorded-run shapes into the machine block so a bullet-shaped
  human annotation is no longer reordered into it or clipped by the cap (R4-13).
- Remove the marker-restore branch: with the run shape pinned, every body that
  matched the dedup carries its marker in head+tail, so it was unreachable (R4-9).
- Cross-reference the sibling split/merge contract in both implementations (R4-5),
  disable SC2016 with rationale on the literal-backtick formats (R4-6), and
  document the label-removal residual gap on the job (R4-11).
- Behavioral witnesses: run-cap, stranded-heading, marker-survival, empty-head
  and empty-after-strip prose fallbacks, and the annotation shape; each guard
  mutation-probed red. Pin the dedup label on the list call too (R4-10).

* fix(ci): document the version-marker dedup gap on the failure-issue job (#9916)

Round 4 removed the unreachable marker-restore branch (R4-9) but left its
residual gap undocumented: the dedup lookup only finds the tracked issue
while the version marker survives in the body, so a human edit deleting
the marker orphans the issue and the next failure files a duplicate. Fold
the marker into the job's existing known-gap note alongside its sibling,
the scope/ci-cd label (R4-11), and record the workflow's new size.
2026-08-26 08:52:56 +00:00
qwen-code-dev-bot
38c5f9b4fd
fix(ci): yield the event loop between script tests to avoid vitest RPC timeouts (#10037) (#10050)
* fix(ci): yield the event loop between script tests to avoid vitest RPC timeouts (#10037)

The v0.22.1 release quality job exited 1 on `npm run test:scripts` with
every test green. vitest's worker->main `onTaskUpdate` RPC has a fixed 60s
timeout; the synchronous spawnSync-driven script suites keep a forked
worker's event loop blocked for an entire file (~66s on the heaviest
suite), so the queued RPC response is never processed before the timer
fires, surfacing as an unhandled `[vitest-worker]: Timeout calling
"onTaskUpdate"` error. Linux keeps unhandled errors fatal (the scripts
vitest config only exempts non-Linux since #9728), so the release died.

Add a global per-test event-loop yield to the scripts test setup. The
timer is captured at setup load so `vi.useFakeTimers()` inside a test
cannot intercept the yield. Any continuous stall is now bounded by a
single test, so RPC responses drain long before the 60s deadline. Real
test failures stay fatal on every platform; the Linux unhandled-error
signal is untouched.

* fix(ci): state the actual yield invariant in the script test setup comment (#10037)

* test(ci): pin the script-test event-loop yield invariant (#10037)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-26 07:11:15 +00:00
Shaojin Wen
0756be0ce7
ci: take the macOS and Windows lanes off pull requests (#10059)
* ci: take the macOS and Windows lanes off pull requests

Both lanes were costing contributors more than they were telling them.
Measured over the 18 hours before this change, on pull requests:

  - Windows reported 13 failures and 0 successes. The failures are one
    standing set of Windows-only path and symlink cases — resolved paths
    into read_many_files, releaseWorktree through an ancestor symlink,
    the SHA-256 review worktree — repeating across unrelated PRs, so the
    red X almost never belonged to the diff under it.
  - macOS queued for a p90 of 42 minutes and up to 159, on a hosted pool
    this repository does not saturate by itself: 20 of the 63 sampled
    waits happened with zero macOS jobs of ours running.

Neither lane gates a merge — the `main` ruleset carries no required
status check — so none of that waiting or noise was buying protection.
Leave them on the nightly, the merge queue and dispatch, and drop the
pull-request arm plus the classifier job that existed only to feed it.

That makes the nightly load-bearing rather than a backstop: it is now
the only report of a non-Linux regression, so add a guard for the three
ways it could go quiet — the schedule disappearing, a lane no longer
accepting it, or a third job joining it and failing the run for reasons
that have nothing to do with either platform.

The classifier, its script mode and both test files are left in place so
restoring the pull-request trigger, once the Windows failures are fixed,
is a revert plus two `if` arms.

* ci: pin the retired-classifier contract in ci-platform-lanes.test.js

The lane change left scripts/tests/ci-platform-lanes.test.js pinning the
shape it removed — a sensitive-PR trigger arm and a live classify_platform
job — which failed the Test job on this branch. Rewrite the suite to pin
the new contract instead: both lanes run on the schedule, the queue and
dispatch and on nothing else; the pull-request arm and the classifier are
gone whole (a half-restoration — a trigger without its classifier, or the
reverse — fails); the classifier's own scripts stay tested so restoring
the trigger stays a clean revert; the nightly still reaches exactly the
two lanes and its failure still files an issue.

That suite already owned the nightly-liveness assertions, so the
platform-lane-triggers.test.mjs guard added earlier on this branch
duplicated it — dropped, along with its HELPER_TESTS entry.

Verified by mutation: deleting the schedule, dropping schedule from one
lane, letting a third job onto the nightly, and reintroducing the
pull-request arm without its classifier each fail the suite; the branch
shape passes 17/17.
2026-08-26 03:31:55 +00:00
易良
50c553550e
fix(live): restore Live Host after desktop removal (#9994)
* fix(live): restore the standalone Live Host

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(release): restore Live Host release feeds

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* chore(skills): remove the unsupported desktop pet skill

* fix(ci): stabilize Live Host validation

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(cli): complete session swap telemetry fixture

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-25 12:32:31 +00:00
Shaojin Wen
519e824dc7
fix(ci): give the macOS and Windows lanes a trigger again (#9370)
* fix(ci): give the macOS and Windows lanes a trigger again

Both lanes are gated on `merge_group`, and no merge queue is enabled on
this repository — the `main` ruleset carries only deletion,
non-fast-forward and pull_request rules, no status check is required,
and merges land as squashes. The last `merge_group` run of anything was
2026-07-02. So the gate was an off switch: the lanes reported as
"skipped" on every pull request, which reads as agreement, and nothing
ever reached them afterwards. The only signal this repository has about
a host that is not Linux with a GNU userland had been silently off for
six weeks, which is how #9220 shipped a GNU-only `realpath -m` in a
workflow guard with the suite that pinned it red on every Mac.

Three triggers now, in cost order.

A pull request whose diff a new classifier recognises as
platform-sensitive: shell scripts of every dialect, workflow and
composite-action YAML and the scripts they call, the script layer and
its tests, the test-runner configuration that decides which suites run
where, the root manifests, and source paths whose segments name a
platform-coupled subsystem. It is a net, not a proof — it cannot see a
platform assumption inside an ordinary source file, and no path rule
ever will — so every unknown answers "sensitive": an unreadable listing,
an unparsable entry, a fork pull request, a truncated file list, or the
classify job failing outright all end as "run the lanes". Only a
confident `false` skips them.

The merge queue, if it is ever enabled again, unchanged.

And a nightly run on `main` for everything the path list cannot see.
Every other job in the workflow excludes `schedule` explicitly, so a
nightly is exactly two jobs, and 'Qwen Code CI' joins the workflows the
main-failure watcher opens autofix issues for — a red lane nobody is
told about is the same silence the queue gate produced. That watcher
gains a trigger-level `branches: ['main']` filter so the CI workflow's
pull-request completions do not raise an event there just to skip.

The classifier runs in its own small hosted job rather than as a step in
`classify_pr`: that job's outputs pick the Linux runner for the whole
run, and this one needs a checkout — on a pool whose workspace other
jobs have poisoned before. It checks out the pull request's BASE commit,
never the head: it runs before any review and executes a script from the
tree it checks out. Its listing goes through the existing
classify-pr-profile.sh wrapper, extended with a mode argument, because
that wrapper's whole point is that one PR is never listed twice and
classified differently in two places.

Twelve tests pin the wiring — the triggers, the fail-safe direction of
the gate, the base-commit checkout, that a nightly stays two jobs, and
the alerting — and ten more pin the classifier itself, including the
substring traps (`Shellfish.tsx`, `cryptic.ts`, `plateauDetector.ts`
must not drag both lanes in) and every fail-safe path. Mutation-checked:
flipping the gate to `== 'true'`, dropping the schedule from a lane,
dropping the nightly guard from the ubuntu job, pointing the checkout at
the head, dropping the classifier's test from the helper list, and
removing the schedule trigger are each caught.

* fix(ci): gate the Windows lane's checkout verification per trigger

The first thing the revived triggers hit was not a test failure but the
lane's own plumbing. `test_windows` verifies its checkout with
`verify-checkout-head`, and the input was written when this lane ran in
the merge queue alone: `expected_sha: github.event.merge_group.head_sha`,
with no event gate. On a pull request that expression is empty, the
action refuses an empty SHA, and the lane went red in 63 seconds without
running a test — the first Windows run in six weeks, failing on the
trigger rather than on the code.

Give it the event-aware shape the Ubuntu gate already uses, and skip it
where there is nothing to verify: the scheduled and dispatch runs check
out a branch by name, not a head commit.

Pinned generally rather than by name: for both lanes, any step whose
inputs read a `github.event.<event>` context must be gated to that
event, in the step's own `if` or in the expression itself. Restoring the
old spelling turns that test red.

* fix(ci): close review round on the revived platform lanes (#9370)

* fix(ci): pin the Windows lane routing to the canonical trust policy (#9370)

* fix(ci): host-probe the macOS lane's bash and GNU dependencies (#9370)

The revived macOS lane ran the shared suite for the first time since the
merge queue went dark and failed on exactly three tests, identical across
three consecutive runs: the bite-check block and the baseline A/B green
path die with `mapfile: command not found` (a bash >= 4.4 builtin; macOS
ships 3.2), and the health-probe repair case trips over BSD `mktemp -u`
attempting to create where GNU's print-only `-u` just names the canary.

Both suites pin scripts that only ever execute on Linux runners —
ubuntu-latest or the Linux ECS pool — so the defects cannot exist in
production; the suites just cannot run those scripts on a macOS host.
Follow the convention #9220's fix established for the realpath case in
the same file: probe the host capability, not the platform, and skipIf
the three dependent tests where it is absent. A Mac with a newer bash or
GNU coreutils fronting PATH keeps the coverage; the Linux lane runs all
three unconditionally. The large bite-check test body is re-indented by
prettier around the new skipIf wrapper; no other token changed.

* fix(ci): stop the subsystem rule matching compounds that name something else

The platform-sensitivity classifier split a path segment on dashes and
underscores anywhere, so `packages/web-shell/**` matched the `shell`
keyword — one of this repository's largest packages, a browser UI with
no host coupling, summoning both expensive lanes on every change to it.
That is the cost the gate exists to avoid, spent on the wrong diffs.

A keyword now counts when it NAMES the thing: a whole path segment
(`src/sandbox/**`, `platform/paths.ts`, `shell.ts`) or the head of a
hyphen/underscore stem (`pty-host.ts`). Not a trailing part of a
compound, which belongs to whatever the leading word names, and still
not a substring inside a longer word.

Pinned both directions, including a directory that IS named for the
subsystem wherever it sits (`web-shell/components/shell/**` stays
sensitive). Mutation-checked: dropping either rule, or restoring the
split-anywhere spelling, turns the suite red.

Also drops a wrong issue citation in the watcher test's comment: the
nightly comes from this change, not from the wipe-guard back-port.

* test(ci): pin the gate as a disjunction and the watcher's name binding

Two mutation-survivable gaps in this PR's own tests.

The clause-presence assertions left a connective mutation alive: `||` →
`&&` between two event clauses keeps every asserted string in place and
makes the gate unsatisfiable for every trigger — both lanes silently off
again, which is the state this PR exists to end. Read the event group and
require it to be a disjunction, allowing `&&` only inside the
pull-request clause that binds to the classifier output.

And the watcher's binding to this workflow is by display name:
`workflow_run.workflows` matches the watched workflow's `name:`, so
renaming ci.yml unhooks the nightly's alerting silently. Pin both sides.

Both checked by mutation: flipping one `||` and renaming the workflow
each turn a named test red.

* fix(ci): close the classifier's CRLF gap and widen the lane step scan

Three findings from this round, all in the direction of the tests and
the parser being less clever than they claimed.

The classifier's JSONL reader split on `\n` while its sibling splits on
`/\r?\n/`. Every suffix rule here is end-anchored, so one carriage
return on a CRLF listing would leave `build.sh\r` and classify a
script-layer change as ordinary source. Matched to the sibling, with a
fixture on both the JSON and the raw-line path.

The per-trigger step scan serialized only `with:` inputs, so the same
defect wearing an `env:` or `run:` key escaped it; it now reads all
three.

And the nightly blast-radius guard tested for the MENTION of an
allowlisted event rather than the IMPOSSIBILITY of `schedule` — a job
gated `pull_request || schedule` satisfied it while running every night.
It now requires the absence of an explicit schedule clause too.

* style(ci): format the runner-routing suite

The Windows trust-policy matrix added last round left the file outside
prettier's style, which the repository's lint step fails on; main's copy
is clean. Formatting only — the nine assertions are unchanged and still
pass.

* fix(ci): gate the two mapfile-crossing gate tests on the host probe (#9370)

* fix(ci): keep pull requests off the persistent Windows pool and cover native audio (#9370)

A pull_request run executes the workflow YAML from the PR's own merge
commit, so the test_windows runs-on trust clause it evaluated could be
rewritten by any PR the lane admits. Every pull request now runs on
hosted windows-2022 unconditionally; the pool is reached only by the
post-approval merge queue, schedule and dispatch, guarded by the
kill-switch. The routing tests and the exact-line pin are re-pointed at
that enforceable shape.

Also add audio to the platform-sensitivity classifier's subsystem
keywords: packages/audio-capture is a node-gyp workspace compiled
per-host on exactly the two revived lanes, but its native sources
(.cc/.mm/.gyp) carried no rule and a PR touching only them skipped both
lanes. The workspace directory now classifies sensitive; an ordinary
.cc elsewhere stays ordinary source.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.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>
2026-08-25 11:53:05 +00:00
Shaojin Wen
b331509de4
fix: repair the Windows and macOS test lane failures (#9728)
* fix: repair the Windows and macOS test lane failures

The platform lanes have been dark since 2026-07-02 (gated on a merge
queue that is not enabled); reviving them in #9370 exposed these
pre-existing failures. 72 failing tests across 16 files, all traced to
platform assumptions:

Product fixes (2):
- daemon-git-worktree-guard: on Windows a backslash is a path
  separator, not a POSIX escape. The shell-quote tokenizer consumed
  `\x` pairs, mangling `C:\repo\sub` into a relative word — false
  denials for legitimate commands AND undetected relocations for
  backslash-relative ones. Preserve unquoted backslashes before
  tokenisation on win32.
- acpAgent isOwnerOnlyDirectory: hard-returning false on win32
  disabled Live managed relocation entirely (Node exposes no
  ownership bits there). Rest on the structural checks — symlink
  rejection and dev/ino identity across the realpath round trip —
  the same trade-off serve/live/discovery.ts already makes.

Test-fixture fixes (the product code was already Windows-correct):
- server.test Live catalog roots use the host-native path shape
  (path.resolve equality proof fails for POSIX literals on win32)
- review cleanup suite pins POSIX node:path semantics for its
  literal-keyed mocks; fetch-pr resume budget uses a native tmpdir;
  scratch-tree clears the DOS read-only attribute before overwriting
  a git-created gitfile; worktree-list assertions compare
  slash-normalized (git prints forward slashes on Windows)
- mode-bit (0600) assertions skip on win32 (no POSIX permission
  bits; every read side already skips its mode check there)
- O_NOFOLLOW symlink test and the unescapePath no-op test skip on
  win32; sidecar errno injection uses a portable NUL byte; Footer
  exact-hint text is platform-conditional (win32 indicator is
  8 columns shorter, shifting the flex shrink by one)

scripts vitest suite: drop the fixed 8-16 worker floor that
oversubscribes the 3-core macOS runners — the main thread stalled
past the 60s worker RPC timeout (onTaskUpdate), exiting 1 with every
test green.

* fix: second round of platform lane repairs from CI verification

Verification run (fixes + #9370's workflow) cut the Windows failures
from 69 to 5 and left the macOS lane's infra error. Follow-ups:

- daemon-git-worktree-guard resolvePhysicalPath: splitting an absolute
  Windows path yields the drive as a segment (C:), which path.join
  glued back onto the root as C:\C:. Walk only the part past the root.
  Exposed by the first round's tokenizer fix, which let intact drive
  paths reach this code for the first time.
- scratch-tree tests: the git-created gitfile refuses in-place
  overwrite on Windows even after clearing the read-only attribute;
  delete and recreate instead.
- bridge.test stderr audit assertion: the line prints the session id
  through JSON.stringify, escaping Windows backslashes; match the
  escaped spelling (test added this morning by #9543, landed after the
  baseline census).
- managed-scratch 'root replaced' test: dev/ino identity is not
  reliably observable on every Windows volume; state the precondition
  and skip where the swap is indistinguishable.
- scripts vitest suite: the unhandled onTaskUpdate worker RPC timeout
  is deterministic on the macOS runners with every test green; stop
  letting unhandled errors fail this suite while test failures stay
  fatal, and drop the stale claim that the pool override removal
  fixed it.

* fix(ci): skip coverage report generation on non-Linux CI

CI consumes coverage only from the ubuntu lane: the artifact upload and
the coverage comment both pin coverage-reports-*-ubuntu-latest. On the
Windows runners the v8 report generation for 800+ files stalls the
vitest main thread past the 60s worker RPC budget at the end of an
all-green cli run, exiting the lane 1 (observed in verification run
32569004418). Skip coverage on non-Linux CI; local runs keep it.

* fix(ci): stop all-green cli/core runs exiting red on RPC timeout

The Windows lane's third verification round repeated the failure with
coverage already disabled: 866 cli test files green, then the worker
onTaskUpdate RPC budget (60s, hardcoded in vitest's bundled birpc)
expired under runner resource pressure and the unhandled error exited
the lane 1. Extend the scripts suite's treatment to the two big package
suites: test failures stay fatal, unhandled errors do not.

* fix: address review on the win32 guard pre-pass and lane configs

R1-1 (Critical): the win32 pre-pass escaped the character after every
unquoted backslash, so whitespace after a trailing separator glued the
next word into the -C value — a second -C/--git-dir/-c parked there
vanished from the analysis while cmd.exe still split the argv at the
whitespace, allowing a destructive mutation outside the boundary. The
tokenizer treats `\<space>` as an escaped space even after an even
number of backslashes, so escaping forward can never express "literal
backslash, then word boundary". Escape the backslash alone instead: a
double-quoted backslash before whitespace and cmd boundary characters
(; | & < > ( )) keeps them their separator role, and a plain escaped
backslash elsewhere. Verified token boundaries for the attack shape,
its tab variant, trailing-separator-before-flag, UNC, chained -C, and
quoted paths; added win32-only guard tests for the boundary shapes.

R1-2/3/4: gate dangerouslyIgnoreUnhandledErrors to non-Linux — the
ubuntu lane and Linux local runs keep the unhandled-error signal.

R1-5: pin the deterministic win32 footer truncation ('queu') instead
of skipping the content assertion there.

R1-6: build the Live conversations fixture root with the file's
documented path.resolve(path.sep, ...) convention instead of a
hardcoded C: literal.

* fix: three Windows lane failures from recent main commits

Verification of the revived lanes surfaced three failures introduced
by commits that landed while the lanes were dark:

- isSameFile compared dev/ino unconditionally; on volumes that report
  ino 0 (or a colliding value) for every file it equated distinct
  files. Treat an unverifiable inode like core's hasVerifiableInode
  convention and fall back to canonical spellings — losing hard-link
  identity there, but never equating distinct files. The hard-link
  test skips where the volume exposes no inode.
- drive's bound-address recipe test rmSync'd its temp dir while the
  backgrounded service still held it (EBUSY on Windows); shorten the
  service's self-exit timer and retry the removal.
- the FileReadCache seeding test collided under one dev:ino key when
  the volume reports the same inode for both MEMORY.md indexes; skip
  where inode identity is not real.

* fix: address round-2 review on identity fail-closed checks and the win32 guard pre-pass

* fix(cli): deny cmd.exe rewrite syntax in the daemon git-worktree guard (#9728)

* fix: address round-4 review on the cmd-rewrite denial reason and cmd-lane test gating

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix: address round-5 review by closing the divergent Windows shell surface structurally

The win32 cmd/PowerShell lanes were analysed through a POSIX text model
plus per-stage patches; each round closed one entrance of the divergence
family and the next found new ones. Fail closed on syntax whose lane
semantics diverge from the model (lone `&`, `( )`, cmd `#`/`;`/single
quotes, /s outer-quote strip, PowerShell `--%`/`''` doubling), normalize
the whole command text once before any stage reads it, drop the bash
shadow model on lanes where the syntax defines nothing, and stop scoping
PowerShell pipeline stages as subshells.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix: address round-6 review by gating bash-semantics tests off the win32 lane and failing closed on nested Windows shells

R6-1: the whole-text divergent-syntax gate denied ~20 ungated bash-semantics
expectations on the real win32/cmd merge lane (41 failures reproduced under a
lane-spoof harness). Gate those blocks off the win32 non-bash lanes, splitting
mixed blocks so lane-safe pins keep running there, and commit the lane-spoof
harness so the whole guard suite runs as the win32/cmd merge lane on every
lane.

R5-1 (partial): cmd/powershell/pwsh invocations now fail closed on the
undecidable-payload denial on every lane — their payloads are parsed by a
grammar the POSIX text model cannot read (closes the -EncodedCommand and
nested cmd /c entrances probed at this head). The remaining class closure is
escalated to the maintainer as a product/scope decision.

* fix: address round-7 review by gating the Windows-shell fail-closed rule to the win32 platform

R7-1: the round-6 WINDOWS_SHELL_PROGRAMS branch failed closed on every lane,
which denied benign cross-platform PowerShell on POSIX daemons (pwsh
-NoProfile -Command Write-Output hello reproduced denied on the unresolved
reason at this head). Gate the rule to platform win32 — cmd.exe and Windows
PowerShell only exist there, and they stay reachable from win32 Git Bash
sessions, so the gate is platform-wide rather than windowsNative; on POSIX
the same names keep the base stance of ordinary unmodelled programs. Move
the fail-closed pins onto spoofed win32 lanes, add a win32 Git Bash lane pin
so the entrance stays closed there, and add the POSIX benign-pwsh regression
test.

* fix(cli): close the cmd.exe state-persisting entrances in the daemon guard

Probe-verified bypasses (review R5-1): cmd.exe builtins persist state
into every later &&-chained command, and the analysis modelled none of
them — `set GIT_WORK_TREE=<outside>&& git reset --hard` and
`chdir <outside> && git reset --hard` both analysed cwd-local while
the executed command relocated outside the boundary.

cmd's state-persisting builtins are a closed set, so this enumerates
them instead of chasing individual shapes:

- `set VAR=value` / `setx VAR value` carry the semantics of a POSIX
  `export VAR=value` for every later segment; route them through that
  machinery (GIT_* keys become relocations, unresolvable keys fail
  closed). Non-assignment forms (`set /p`, dynamic operands) fail
  closed.
- `chdir` joins the cd family (cmd's synonym), `/D` consumed as the
  drive-switch option.
- `path` and `doskey` rewrite which executable a bare name resolves
  to — unresolvable, failed closed.
- copy/mklink/move/robocopy/xcopy join the relinking programs on
  win32, and the text relocation markers learn `chdir`.

PowerShell-only entrances (function definitions, New-Item function:,
@-splatting) already fail closed through the unmodelled-syntax gate;
the new branches stay gated off Git-Bash sessions, where these words
are ordinary POSIX commands. Pinned by new tests in the win32-lane
harness, which spoofs win32/cmd on every platform.

* Revert "fix(cli): close the cmd.exe state-persisting entrances in the daemon guard"

This reverts commit a8f137a418.

* Reapply "fix(cli): close the cmd.exe state-persisting entrances in the daemon guard"

This reverts commit b888a42d9f.

* fix: address round-10 review by closing the Windows-lane guard entrances and the ino-0 case fold

* fix(cli): repair the round-10 build rejection by mapping chdir variants exhaustively

* fix(cli): make the /MIR relink-switch denial assertion lane-independent

The assertion spelled the unresolvable target as POSIX '/MIR', but the win32
lane resolves it through path.win32/realpathNearestExistingAsync into a
backslash spelling ('\\MIR'/'C:\\MIR'), so the new test failed only on the
merge_group-gated test_windows lane. Match either separator spelling.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-25 11:49:27 +00:00
qqqys
b449a9536a
feat(channels): add DingTalk Workspace channel (#9394)
* feat(channels): add DingTalk Workspace channel

Add a DingTalk Workspace (DWS) channel package so a workspace can be
driven from DingTalk alongside the existing channels.

- packages/channels/dws: new workspace holding the DWS client, event
  stream, environment resolution and channel implementation, with the
  event-source fixtures used by its tests.
- cli: register DWS in the channel registry and its builtin list.
- web-shell: recognise the DWS platform in the channels UI.
- docs: document the channel and its configuration under
  docs/users/features/channels.
- build/release: include the new workspace in the build, clean and
  release-version scripts and the vitest project list.

The channel watches native DingTalk todos, routes document and todo
replies back to their originating conversation, bounds notification
retries, and keeps sender identity authoritative for direct messages.

* fix(dws): classify spawn-resource errnos as not sent, and test against base's source

Round 1 review, two Critical findings.

vitest.config.ts — the new package's config was the only channel config
without the `@qwen-code/channel-base` → source alias its five siblings carry,
so `cd packages/channels/dws && npx vitest run` (the workflow AGENTS.md
prescribes) depended on a prior `tsc --build` of base. Reproduced the
reviewer's witness in this worktree with base/dist moved aside: without the
alias vitest dies in `packageEntryFailure` and runs zero tests; with it,
63/63 pass. Even when dist exists it may lag base's source — it did here, by
four days.

dws-client.ts — `DWS_NOT_SENT_ERROR_CODES` listed only the path errnos, so a
`dws` process that never started because of fd or memory exhaustion
(`EMFILE`/`ENFILE`/`ENOMEM`/`EAGAIN` and family) was classified `unknown`.
The todo and document reply paths in dws-channel.ts swallow `unknown` as
"the originating task will not be rerun", so a user's final reply was dropped
permanently on one log line instead of being retried — and the retry is safe,
since the fingerprint is not persisted when delivery fails. The set now
carries the whole `uv_spawn` pre-exec family. Everything else the callback
reports — a non-zero exit (numeric `code`), a timeout kill (`code === null`),
`ABORT_ERR`, a `maxBuffer` overrun — happened with a child already running
and stays `unknown`, because a retry there could duplicate a delivery.

The classification moved into an exported `classifyDwsCommandFailure` so the
table can be driven directly: the resource errnos need real fd or memory
exhaustion to reproduce through a spawn, which no unit test can stage safely.
The existing missing-executable test still covers the wiring end to end.

Verified: packages/channels/dws — 191 passed (5 files). Mutation-verified:
reverting the errno set turns exactly the 12 added codes red (12 failed /
51 passed); dropping the vitest alias with base/dist absent turns the suite
from 63 passed into a collection failure. eslint and prettier clean. The one
tsc error on this branch (`displayText` missing from `Envelope`) is worktree
build skew — base/dist was built 2026-08-10, base/src changed 2026-08-14, and
the field is present in the source; it reproduces identically with these
changes stashed.

* fix(dws): stop a denied sender from consuming a document comment's dedup slot

Round-2 review, R2-4 (Critical).

`notificationKey` is `documentNotificationKey(documentId, commentKey)` — no
sender in it — so a `'denied'` outcome falling into the `else` branch marked
that (document, comment) pair processed for good. Every later notification for
the same comment, live or polled, then hit
`processedMessages.includes(notificationKey)` and returned silently, including
one from a sender who IS allowed. The cursor persists, so the drop survived
restarts.

Concretely, with `senderPolicy: 'allowlist'` and `allowedUsers: ['open-bob']`:
Alice (not allowlisted) @-mentions the bot in a document comment and is denied;
Bob then mentions the bot on the same comment thread — the ordinary
multi-reviewer document flow — and is dropped forever, with no dispatch, no
pairing and no log.

A denied notification is now parked with `rememberPendingDocumentNotification`
like a `'pairing'` one rather than consuming the slot. Replay already skips a
pending entry whose sender fails `gate.isAllowed`, so a denied sender does not
get retried in; and an allowed sender reaching the same comment clears the
entry on the way through.

The existing `applies sender access policy to document mention notifications`
cannot cover this — its denied and allowed notifications are on DIFFERENT
comments, so the shared key is never exercised. New test puts both on the same
comment. Mutation-checked: restoring the old condition reddens it with
`bridge.prompt` called 0 times against an expected 1, reproducing the review's
own witness.

Verification: `npm run build` and `tsc --noEmit` clean in packages/channels/dws;
eslint clean on both changed files; full package suite 192/192 (118 in
dws-channel.test.ts, 1 new).

* fix(dws): stop a poison message, a full pending queue, and an unreachable
replay from pinning the watermark (R2-1, R2-2, R2-4 queue)

Three ways history polling could stall forever, each measured:

**R2-2, poison message.** A message whose turn threw was never marked
processed, so the watermark never advanced and every poll re-ran it as a
full agent turn — one model call per iteration, no cap, no backoff —
while the pinned watermark grew the query window without bound and the
throw starved every newer message behind it. Pending-document replay
already had retry accounting; this path had none. Inbound failures are
now counted per message and persisted in the cursor: under budget the
error still propagates (redelivery retry and the concurrent-duplicate
contract depend on that, and their tests pin it), and once the budget is
spent the message is marked processed and dropped with a logged reason.

**Pending-queue cap.** `rememberPendingDocumentNotification` threw at
MAX_PROCESSED_ITEMS, and the throw aborted the direct-message loop
before the checkpoint, the watermark and `markProcessedMessage` — so
every later poll re-scanned a growing window and re-threw on the same
never-marked message, surviving restarts in the cursor. The queue's only
drain is an allowed sender later processing the same comment, so entries
parked for unapproved senders never leave: one unpaired member
@-mentioning the bot in 5,000 distinct comments broke document history
polling until manual cursor surgery. It now evicts the oldest instead,
which costs at most a pairing prompt nobody approved.

**R2-1, the replay the fixture could not recover.** The test fake
ignored its `startTime`/`endTime`, so it certified a recovery the
production arithmetic cannot perform. Fixed on both sides: the fake now
filters by its window like the real client (and `message()` defaults
`eventTime` to now, since real messages always carry one — six fixtures
were silently relying on epoch 0), and the stale-replay guard now pulls
`notificationWatermark` back to the parked notification's event time. It
parks document notifications UNMARKED on purpose, "for polling to
recover"; on a fresh cursor the watermark started at
`connectionStartedAt` and the window opened at `watermark − 5s` —
exactly the guard's own drop boundary — so everything it parked was
strictly outside every window that watermark would ever produce.

Every fix is mutation-verified: reverting the retry budget re-runs the
poison turn once per poll (8 polls, 8 turns), restoring the queue throw
reproduces the reviewer's stderr and the pinned watermark, and dropping
the watermark pull-back leaves the replayed notification unrecovered.
Suite 194/194 green; `tsc -p packages/channels/dws` clean.

R1-2 (self-identity degradation) is not in this commit — both fixes the
review proposes collide with behaviour this suite pins deliberately; see
the thread.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(dws): budget every inbound surface, and stop restarting a dead source (R4-1, R4-3)

R4-1: round 3 added an inbound failure budget, but wired it into one of the
three `handleInbound` call sites — the mention/live-IM path. The other two kept
the exact unbounded-retry mode the budget's own doc comment says it exists to
close.

- Document notifications (`processDocumentNotification`): a throw escapes
  `pollOnce`'s sorted loop and is swallowed by the outer catch, so nothing is
  marked processed and `notificationCheckpoint`/`notificationWatermark` — both
  assigned after the loop — never advance. Every 5s poll re-ran the same full
  agent turn, forever, starving every newer notification behind it.
- Native todos (`pollTodos`): the fingerprint is remembered only on success, so
  a todo whose turn keeps throwing was re-fetched and re-run every poll,
  forever.

`recordInboundFailure` now takes the drop action as a parameter, because "stop
re-running this" differs per surface: marking the key processed is right for a
message, a document notification carries its own `notificationKey` (and a
pending entry to clear), and a todo is re-fetched by fingerprint. The default
keeps the mention path byte-identical.

R4-3: `retryable: false` is terminal before ready — `retryLimit` returns 0 —
but `scheduleImRestart` never consulted it, and `startImSource` resets
`restartAttempts` to 0 every time a subscription becomes ready. The backoff
exponent therefore stayed at 0, so a permanently denied consumer (permission
revoked, subscription not allowed) was respawned at a constant ~3s forever —
one `dws event consume` child every 2-3s per affected source — while the
channel reported itself connected and delivered nothing for that source.
Post-ready now matches pre-ready: terminal, with a log line saying so.

Verification (`cd packages/channels/dws`):
- `npx vitest run` — 197 passed (was 194; three new tests).
- `npx tsc -p tsconfig.json --noEmit` — clean.
- Mutation checks, one per fix, each turning exactly its own test red and
  leaving the other 122 green:
  - drop the `retryable === false` guard -> `stops restarting a source that
    died permanently after becoming ready` fails.
  - drop the document-path budget -> `drops a document notification whose turn
    keeps failing, and stops starving newer ones` fails (the newer
    notification is never reached).
  - drop the todo-path budget -> `drops a native todo whose turn keeps
    failing` fails (8 turns instead of 5).
- eslint + prettier clean.

Not addressed in this commit: R4-2 (checkpoint drain overwriting the stale
replay pull-back), R4-4, R1-2, and R4-5..R4-8.

* fix(dws): stop an in-flight poll from clobbering the stale-replay pullback (R4-4)

`handleImMessage` leaves a replayed document notification UNMARKED on purpose,
for history polling to pick up, and pulls `notificationWatermark` back to the
replay's `eventTime` so a future window can reach it. `pollOnce` then wrote
`checkpoint.endTime` over that watermark unconditionally when its own window
finished — and `checkpoint.endTime` is always past the replay's `eventTime`.

The race is not hairline: `runLoop` polls immediately on connect and the IM
subscriptions start before the poll loop, so a startup replay arrives precisely
while poll #1's `listDirectMessages` is awaiting. One clobber puts the parked
replay outside every window the watermark will ever produce — no turn, no log,
no error, and it survives restarts because `saveCursor()` persists it.

`pollOnce` now records whether the watermark was pulled back while its
direct-message fetch was in flight, and on that path drops the window instead of
finishing it: neither the advance nor the paginated checkpoint resume is safe,
because the checkpoint was itself derived from the pre-pullback watermark. The
next poll re-derives a window from the pulled-back value.

Test: `keeps the stale-replay pullback when a poll was already in flight` emits
the replay from inside `listDirectMessages`. Mutation-checked — forcing the
guard false reddens it with `inbound` empty, matching the reviewer's witness
(`dispatched = 0`). It also asserts the second query window opens at or before
the replay's `eventTime`, so a fake that ignored its window could not certify it.

* fix(dws): stop three silent, permanent losses of a document mention (R6-1/R6-2/R6-3)

All three Criticals round 6 raised share a failure shape: a document comment is
consumed by something that had no right to consume it, the user gets no reply,
and nothing is logged. Each is fixed at the point that consumes the slot.

R6-1 — `handleImMessage` pullback (dws-channel.ts): R4-4 rescued a stale replay
by pulling the notification watermark back, but the flag `pollOnce` consults is
cleared at the top of every fetch, so it only ever covered a replay that landed
DURING one. A pullback arriving in the gap between two polls is reset before it
is read; a persisted multi-page `notificationCheckpoint` then resumes a window
that starts after the replay and finishes by writing `checkpoint.endTime` back
over the pulled-back watermark. The replay was left unmarked on purpose, so
after that no window ever reaches it again. The pullback branch now drops the
checkpoint as well, which makes the rescue durable regardless of when the
replay arrived; the in-flight flag still guards the during-a-fetch case.

R6-2 — in-flight awaiter (dws-channel.ts): a pending entry means the in-flight
turn PARKED the comment for a sender it would not serve, which says nothing
about the caller waiting behind it. Marking unconditionally consumed an ALLOWED
sender's mention outright — replay only re-drives a parked entry whose own
`senderId` passes the gate (the denied one never will), and the allowed
sender's marked message key is skipped by every later history poll. The awaiter
now marks only when the comment is genuinely processed, or when this caller is
no more entitled to it than the sender already parked. This is what the
denied-sender comment further down already claimed happened ("an allowed sender
reaching the same comment clears the entry on the way through") — the awaiter
was the path that never let them reach it.

R6-3 — failure-budget drop closure (dws-channel.ts): the closure marked the
sender-agnostic `notificationKey` (`document\0comment`, no sender), so five
failed turns — about 25s of transient model or bridge trouble, since each 5s
poll re-runs an unmarked notification — dropped every FUTURE mention of that
comment from anyone, permanently and across restarts. It now marks only the
failing message's own `key`, which is what stops the window re-running it, so
the R4-1 starvation this budget closes stays closed.

Tests (dws-channel.test.ts), each mutation-verified against the pre-fix code:
- `keeps a stale-replay pullback that arrives between two polls` — persists a
  bounded checkpoint, emits the replay with no poll in flight, asserts the
  checkpoint is released and the next window reaches back over the replay.
  Reverting R6-1: `expected { startTime: … } to be undefined`.
- `lets an allowed sender through while a denied turn on the same comment is in
  flight` — the concurrent counterpart to the existing R2-4 test, which lets
  the denied turn finish first and so cannot reach the awaiter. Reverting R6-2:
  the allowed sender's prompt is never called.
- `lets a later mention of a dropped comment retry with a fresh budget` — five
  failing polls, then a different reviewer on the same comment after the
  outage. Reverting R6-3: `expected [] to deeply equal [ ObjectContaining{…} ]`.

Verification: `npx vitest run` in packages/channels/dws — 201 passed (5 files);
`npx tsc --noEmit -p packages/channels/dws/tsconfig.json` clean; `npm run build`
in that package clean; eslint and prettier clean on both changed files.

R1-2 is untouched: it still needs a maintainer call on which pinned contract
gives, and is not something this commit should decide.

* fix(dws): resolve the sender gate before reading a mentioned document (R7-1)

`parseDocumentMentionNotification` reconstructs `(documentId, commentKey)`
from rendered message text, so a bare alidocs URL in an ordinary DM forges a
mention card the channel cannot tell apart from a genuine platform
notification. `processDocumentNotification` then called
`readDocumentContext` on that attacker-named document BEFORE `handleInbound`
resolved the sender gate, so under the documented default
`senderPolicy: 'pairing'` an unpaired stranger could force this profile to
perform an authenticated read of any document it can reach — a turn the
channel would never serve them.

Resolve `gate.isAllowed(message.senderId)` first and read only for a sender
this channel will actually answer. The envelope already carries a "Document
Markdown was unavailable" fallback, the `preflightInbound` document branch
still parks the mention exactly as before, and
`replayPendingDocumentNotifications` re-enters this path once the sender is
approved, so an approved turn still gets its document context — just after
the gate instead of before it.

BEHAVIOR FLIP: `replays a pairing-pending document mention after approval`
pinned `readDocument` being called once for the still-unpaired sender and
twice overall. That pinned expectation was the defect: it asserted an
authenticated read driven by a sender the gate had already refused. It now
expects zero reads before approval and one after. Verified by mutation —
reverting the guard turns both this test and the new forged-mention test red.

Still open on this class and NOT addressed here: the pairing-code write into
the attacker-named comment thread. Closing that needs either fail-closed
verification that `commentKey` is a real comment on `documentId` mentioning
this profile (no DWS CLI surface exposes it — `listMentionedMessages` covers
group IM, not document comments) or structured mention events, so it is a
maintainer contract call rather than a local fix.

Verification:
- packages/channels/dws: 202 passed (5 files), including the new
  `does not read a forged document mention before the sender gate resolves`
- tsc --noEmit -p packages/channels/dws/tsconfig.json: clean
- eslint + prettier --check on both changed files: clean

* fix(dws): list the dws channel as a cli test build prerequisite

`channel-registry.ts` dynamically imports `@qwen-code/channel-dws`, whose
package.json resolves the bare specifier to `dist/index.js` and which
`packages/cli/vitest.config.ts` does not alias to source. It therefore
belongs in `DIST_PREREQUISITES['packages/cli']` alongside every other
builtin channel, so a cli test run on an unbuilt checkout reports the
actionable "run npm run build" message instead of a raw resolution error.

This is what the required `Test (ubuntu-latest, Node 22.x)` check caught
on 4bf040766c: scripts/tests/vitest-global-setup.test.js asserts the list
stays in sync with the registry, and dws was the one registry import
missing from it.

Verified: `npx vitest run scripts/tests/vitest-global-setup.test.js`
29 passed; reverting this one line reproduces the CI assertion exactly
("missing prerequisite entry for packages/channels/dws"), 1 failed | 28
passed. prettier --check and eslint clean.

* fix(dws): close current review blockers

* test(dws): pin fail-closed self identity gate

* fix(dws): preserve retryable inbound work

* fix(dws): preserve in-flight catch-up mentions

* fix(dws): align channel-base on the workspace version so npm ci resolves

`Dependency CVE audit` has failed every run with:

    npm ci can only install packages when your package.json and
    package-lock.json are in sync.
    Missing: @qwen-code/channel-base@0.21.11 from lock file

The diagnosis of "stale base" was right, but the stale file is this PR's
own. `packages/channels/dws` was written when the workspace was at
0.21.11 and pins that version; every sibling channel — dingtalk, feishu,
github, gitlab, qqbot, telegram, wecom, weixin — now says 0.21.14, which
is what `packages/channels/base` actually publishes. A workspace package
cannot satisfy 0.21.11, so npm resolved `@qwen-code/channel-base` for dws
from the REGISTRY instead of linking the sibling, leaving a nested
`packages/channels/dws/node_modules/@qwen-code/channel-base` entry that
`npm ci` refuses. Merging current main cannot fix it: main is not where
the pin lives.

Bump dws to 0.21.14 for both its own version and its channel-base
dependency, matching every sibling, and regenerate the lockfile. The
nested registry entry is gone and dws now links the workspace like the
others. `npm ci --dry-run` completes, and dws typechecks and passes all
211 tests against the workspace channel-base rather than the published
0.21.11 it was resolving before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VgTjRF91xANQh6SY9YGyCf

* fix(dws): replay failed direct messages, and close open review items

* fix(deps): bump tar to 7.5.22 to unblock CVE audit (#9394)

The 2026-08-21 advisory GHSA-r292-9mhp-454m flags tar <= 7.5.20 as
high severity, failing the Dependency CVE audit gate. Main already
moved to 7.5.22 in #9703, but that landed after this branch's last
merge of main. Bump the lockfile entry in-range (core/cli declare
^7.5.19) to match main, and regenerate the committed NOTICES.txt
artifact whose freshness is enforced by CI.

* fix(dws): unblock npm ci, add publish metadata, and keep todo fetch failures out of the turn budget (R13-1, R13-2, R14-1)

* fix(dws): dedup threaded pairing comments on a persisted marker instead of the rotating code (R15-1)

* fix(dws): clear the todo pairing marker when pairing resolves, not on turn success (R16-1)

* fix(dws): address current review blockers

* fix(dws): satisfy event fixture lint

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-25 06:40:47 +00:00
易良
ce72ddbe6c
feat(desktop): remove packages/desktop after OpenWork fork; keep the Tauri upgrade bridge (#9085)
OpenWork (modelstudioai/openwork) has forked the Electron desktop code and is
self-contained now, so retire the Electron package and its release/sync
machinery from this repo:

- Delete packages/desktop (Electron app, live-host app, bun workspace).
- Retire scripts/desktop-openwork-sync.ts and the desktop-openwork-sync root
  script; the OpenWork sync is no longer needed.
- Retire .github/workflows/live-host.yml, live-host-release.yml and
  sync-live-host-to-oss.yml; live-host releases now live in OpenWork. The
  CLI-side packages/cli/src/serve/live code stays for now (separate cleanup).
- Retire scripts/check-voice-guard-sync.js (cli<->desktop parity only) and
  its CI step.
- Clean up remaining references: root package.json workspaces negation and
  package-lock.json, eslint/prettier/yamllint ignores, architecture docs,
  web-shell skill descriptions, and review-lib workspace fixtures/comments
  (renamed to point at packages/desktop-shell, the remaining negation).

Deliberately kept: the Electron->Tauri upgrade bridge — desktop-release.yml
(incl. the electron_bridge input), create-electron-bridge-manifest.mjs,
sync-desktop-to-oss.yml (mirrors Tauri desktop-shell artifacts only) and
everything under packages/desktop-shell.
2026-08-25 05:26:11 +00:00
Shaojin Wen
e89a32c18c
fix(ci): neutralize legacy ##[ commands in autofix stdout echoes (#9871)
* fix(ci): neutralize legacy ##[ commands in autofix stdout echoes

A review finding about workflow-command injection carried its payload
strings (::error::forged, ##[add-matcher]) verbatim as example text.
Prepare echoed feedback.md raw, the runner parsed the mid-line
##[add-matcher] and failed the step before the agent ran, and the
consecutive-failure breaker burned the takeover window of #9761 in
about 70 minutes while reporting it as a 100/100 round cap.

The existing neutralization covered only the modern :: syntax; the
runner also parses the legacy ##[name] form, even mid-line. Extend
every untrusted-content echo in the autofix family (prepare feedback
echo, both artifact dump loops, the gate's failure/handoff/no-action
echoes, the deferred-findings dumps and upsert re-emit loops) to
neutralize both prefixes, and pin the census plus a behavioral ##[
case in the contract tests.

* fix(ci): unify the autofix neutralizer on one canonical spelling

Review feedback on #9871: the two upsert re-emit loops neutralized with
a hybrid spelling (bash expansion for :: plus a single-syntax sed for
##[) while the other fourteen sites used the canonical two-expression
sed, establishing two implementations of one normalization. A future
edit fixing one spelling could miss the other and recreate the
single-syntax half-guard that let #9761 through. Unify both loops on
the canonical sed and consolidate the pinned test shapes.

The census test's comment also promised more than its assertions
deliver: it pins the known enumeration, it does not auto-detect new
un-neutralized echoes. Reword it to state the actual guarantee, ban
the ##[-only sed and the bare bash expansion alongside the existing
::-only ban, and fold the script-side re-emit loop into the census.

* test(ci): assert the neutralizer pairing property instead of banning shapes

The three shape bans only matched the no--e spellings, so the canonical
line's natural half-copies (sed -e 's/::/;;/g' alone, or the ##[-only -e
mirror) passed every ban while guarding only one syntax. Assert the
pairing property instead: every occurrence of either substitution
expression must belong to the canonical two-expression pair, so a
half-guard in ANY spelling unbalances the count and fails the census —
verified by probe against all four half-guard shapes.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-25 02:17:46 +00:00
Shaojin Wen
74ca981bce
fix(ci): scope workflow-size ratchet to the PR that grew the file (#9931)
* fix(ci): scope workflow-size ratchet to the PR that grew the file (#9904)

A workflow that grew on main without a same-PR baseline bump red-walled
every other open PR's CI (recurred twice in two weeks). Given the PR's
base commit, the growth branch now downgrades to a warning when the PR's
copy of the file is byte-identical to the base; a PR that actually
changes the file still fails closed, as does any unresolvable base.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): single-quote the base-sha env value for yamllint

The repo's yamllint config requires single-quoted strings; the value
contains no single quotes, so the double-quoted form failed the
quoted-strings rule in CI.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): align workflow-size ratchet mirror with the PR-scope downgrade (#9931)

Address review round 1:

- Single-quote the WORKFLOW_SIZE_BASE_SHA value; the double quotes
  violated yamllint's quoted-strings rule and prettier's singleQuote,
  hard-failing this PR's own Test lane.
- Wire the base SHA into every `npm run test:ci` step and grant the
  vitest mirror the same stale-baseline leniency as the shell gate;
  the mirror is the only enforcer on the merge-queue lanes, and
  without this the red wall just moves from the gate into test:ci.
- Apply the downgrade to the missing-entry arm too (same red-wall
  shape as a stale size).
- Return a distinct status for an unresolvable base and add a
  diagnostic, so a transient fetch failure is not annotated like
  genuine PR growth.
- Qualify the success banner when stale-baseline warnings fired.
- Deduplicate the hermetic-env construction, pin the brand-new-file
  class, and split the git fixtures into their own describe so the
  strict-path tests still run on git-less runners.

* fix(ci): close review gaps in the workflow-size ratchet mirror (#9931)

- Apply the #9904 stale-baseline leniency to the mirror's entry test, not
  only the allowance test: a workflow that reached main without a baseline
  entry used to warn in the bash gate but hard-fail every unrelated PR in
  `npm run test:ci`, relocating the exact red wall this PR removes.
- Distinguish an unresolvable base from a changed file: fileMatchesBase now
  throws with "re-run the job" guidance instead of folding into false behind
  a message blaming the PR's growth, and the fetch stderr reaches the log.
- Hoist WORKFLOW_SIZE_BASE_SHA to the workflow-level env block so every lane
  inherits it; delete the four hand-wired step-level copies and point the
  tripwire test at the single source.
- Bump the ci.yml baseline entry to the file's true post-hoist size.
- Harden the mirror's witnesses: fixtures hoisted out of the bash-gated
  block so they run on the merge-group Windows/macOS lanes, a backslash-path
  case pinning the pathspec normalization on every lane, a shallow-clone
  fixture pinning the mirror's fetch arm (the production path), and a parity
  fixture running the bash gate and the JS mirror on the same repo state.

* fix(ci): address round-3 review on the workflow-size ratchet (#9931)

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-25 01:46:34 +00:00
qwen-code-dev-bot
65c2bb01c0
fix(ci): narrow serve-ab's self-hosted wipe to the A/B checkout dirs (#9228)
* fix(ci): narrow serve-ab's self-hosted wipe to the A/B checkout dirs

'Wipe stale workspace before checkout' deleted the whole shared workspace
including the root .git, forcing the next job on that runner (e.g. a
fetch-depth: 0 review job) to re-download the full ~900 MB of history from
github.com. On the ECS pool's slow link that stalls checkouts for 20+
minutes and the fetches drop mid-pack often enough to read as hung runners
(2026-08-15: 20 orphaned tmp_pack files, ~6 GB, across 10 runners; one
checkout re-downloaded 890 MB in 19m45s).

serve-ab only builds inside its own head/ and base/ checkouts and never
reads the workspace root, so removing just those two dirs keeps the
anti-bleed guarantee without destroying the shared object store. The
ci-runner-routing pin now asserts the narrow scope and fails on a
whole-workspace wipe regression.

* test(ci): derive serve-ab wipe pin from the checkout paths (#9228)

Address review suggestions: the wipe targets are now derived from the
actions/checkout steps, and the wipe must be exactly one executed
(non-comment) rm line covering exactly those paths. Renamed checkout
paths, appended whole-workspace wipes, and commented-out or echo'd rms
now all fail the suite, while the reverted find-form still does.

* test(ci): pin the serve-ab wipe's full executed script and step order (#9228)

* fix(ci): match serve-ab's wipe step name and pin to its narrowed scope (#9228)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): exclude the shared root .git from serve-ab's self-hosted wipe (#9228)

Replace the head/base-only rm with the exclusion-based find suggested by the R5-3 review: everything is wiped except the shared root .git, which closes the recurring materializer-coverage class (R4-4/R5-1/R5-2/R5-3) at the layer that owns it — anything landed outside .git is removed by the next run's wipe, no matter how it was materialized. This removes a strict subset of what main's whole-workspace wipe removes, so it cannot regress base behavior, while keeping the .git whose destruction forced ~900 MB re-fetches on the ECS pool's slow link. The pin now also locks the execution chain (shell wrapper, no continue-on-error, no BASH_ENV at step/job/workflow level) per R5-4, and the obsolete clone-coverage scan is deleted.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): keep only a real .git and scrub its exec state in serve-ab wipe (#9228)

* fix(ci): follow a symlinked workspace root in the serve-ab wipe (#9228)

* fix(ci): defang the worktreeConfig bypass and anchor the serve-ab wipe scrub (#9228)

The kept-.git wipe tail had two measured holes: extensions.worktreeConfig
activates .git/config.worktree, a second local file that git config --local
neither lists nor unsets, so a planted core.hooksPath survived the allowlist
sweep; and after the heal unlinks a symlinked workspace root the step's CWD
still is the link's target, so the CWD-discovered scrub wrote outside the
workspace. Add qwen-triage's hardened defang pair (delete config.worktree,
unset the extension) and anchor every git call to $WS/.git. Both findings
reproduced locally before the fix; both suites pin the new lines and are
mutation-tested.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-24 13:27:38 +00:00
顾盼
37cedea5b2
feat(computer-use): replace built-in tools with bundled skill (#9856) 2026-08-24 11:05:23 +00:00
Shaojin Wen
b4a54a9f05
fix(ci): move undeletable workspace residue aside instead of leaving it (#9868)
A leftover the pre-checkout sweep cannot delete does not just fail the job
that finds it — it poisons the checkout of every later job scheduled onto
that runner. Measured on run 32621267802: residue from a review probe
(`.qwen/tmp/review-pr-9748-scratch-verify-…/probe-ws/.qwen/tmp/review-pr-666`)
survived on one shared-pool member, and two unrelated PRs then died at
Checkout with the same EACCES on the same path. The sweep's own steps had
reported success: their ladder ends at a warning, so a workspace it cannot
repair is handed to actions/checkout unchanged.

The recovery the ladder was missing is a rename. Unlinking an entry needs
write permission on the directory holding it — exactly what foreign-owned
residue denies on a pool member without passwordless sudo — while renaming
needs it only on the two parents, and the workspace root is always the
runner's own. So a tree that defeats rm, chmod, and chown still moves out of
the way, and the checkout finds nothing to trip on. The destination sits
next to the workspace so the rename cannot cross a filesystem and degrade
into copy-then-unlink, and the warning names it, because the tree still
needs a human.

Reproduced in a Linux container with real foreign ownership (residue owned
by root, workspace by the runner user, sudo unavailable): before, the step
warns `leaked .qwen`, the tree stays, and a checkout-style wipe fails with
`Permission denied`; after, the tree is quarantined, the wipe succeeds, and
the warning points at where it went.
2026-08-24 08:58:16 +00:00
易良
a60cbbc54a
refactor(core): make utils/ a leaf layer (#9778)
* refactor(core): make utils/ a leaf layer

Eliminate every runtime (value) upward import from
packages/core/src/utils production modules so utils/ can become a leaf
layer with no runtime dependency on the rest of core.

Two mechanisms, no behavior change:

- Relocate domain-coupled modules out of utils/ into their owning
  module (agents, config, core, memory, services, tools), and move
  generic constants/types that live elsewhere into utils/. All
  `git mv` moves keep history; every import that pointed at a moved
  file is rewritten.

- Extract the remaining value imports as small leaf modules inside
  utils/ (AuthType, isTool, ToolErrorType, DEFAULT_QWEN_MODEL) and
  re-export them from their original owners so cross-package consumers
  are unaffected. doesToolInvocationMatch moves into shell-utils, its
  only production consumer.

Only type-only imports now cross the utils/ boundary. The two deferred
inversions in debugLogger (Storage, getTraceContext) are stateful and
left for a follow-up.

* chore(core): enforce utils/ leaf layer with lint rule

Add architecture/no-core-utils-upward-import, which flags runtime
(value) imports that leave packages/core/src/utils. Type-only imports,
sibling utils imports, and external package specifiers stay allowed;
the two deferred debugLogger inversions (config/storage,
telemetry/trace-context) are carried on an explicit allowlist.

Enable the rule as an error on core sources and cover it with
Linter-based tests.

* fix(core): restore iconv-lite tree-shaking for sync-file-encoding

The utils leaf-layer refactor moved sync-file-encoding from utils/ to services/, but the esbuild tree-shake plugin still matched the old ./utils/ specifier, so its sideEffects:false marker no longer applied and the ACP startup closure regained a static iconv-lite import. Point the onResolve filter at the new ./services/ path.

* fix(ci): catch stale integration imports earlier

* fix(core): close utils boundary review gaps

* fix(core): close self-reference boundary gaps

* ci: re-trigger after self-hosted runner checkout EACCES
2026-08-24 07:43:01 +00:00
Tianyuan
a2e458deef
feat(auth): add Kimi (Moonshot AI) as a built-in third-party provider (#9814)
Adds a Moonshot preset to the /auth Third-party Providers menu, offering
the international and China API endpoints and seeding the current Kimi
model catalog. Moonshot speaks the OpenAI protocol, so this is a
declarative preset with no new mechanism and no change to the provider
type.

Model metadata follows Moonshot's published capabilities. K3 is marked
thinking-mandatory: its API exposes a reasoning-effort knob but no way to
turn thinking off, so a disable shape must never reach the wire. The two
code models and K2.6 keep thinking toggleable, and all four accept image
and video input, which the K2.6 guide states explicitly.

Registers the new credential env key everywhere a provider key has to
appear: the no-AK CI gate and its pinned assertion list, and the
telemetry provider mapping, both by env key and by request hostname so
Kimi traffic is attributed rather than reported as unknown. The three
first-run docs that enumerate built-in providers are brought back into
agreement, which also picks up entries that were already stale.

Closes #9197
2026-08-24 03:18:40 +00:00
ytahdn
747dbf00c2
feat(web-shell): compact agent activity summaries (#9657)
* feat(web-shell): compact agent activity summaries

* fix(web-shell): preserve merged todo timing

* fix(web-shell): address compact summary review

* test(web-shell): cover compact review edge cases

* fix(web-shell): make the folded thought header row fully clickable

* fix(web-shell): guard folded thought toggle against popover clicks (#9657)

* test(web-shell): cover thinking header hit area and parallel-agent thoughts (#9657)

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-24 02:46:07 +00:00
易良
56db17bd4c
refactor(cli): enforce utils leaf-layer dependency direction (#9146) (#9737)
* refactor(cli): enforce utils leaf-layer dependency direction (#9146)

Move domain-coupled modules out of packages/cli/src/utils into the
directories that own them: config/ (dialogScopeUtils, settingsUtils),
i18n/ (languageUtils), ui/ (handleAutoUpdate, standalone-update,
systemInfo, systemInfoFields, update-relaunch, commands, doctorChecks),
nonInteractive/ (nonInteractiveHelpers, chat-recording-failure,
tool-result-boundary-diagnostics, permission-suggestions), serve/
(sandbox), services/housekeeping/ (scheduler, non-interactive-scheduler),
and commands/review/ (findings).

Extract the generic normalizePartList helper into
utils/normalize-part-list.ts so utils consumers keep importing downward,
and move the MergeStrategy enum into utils/deepMerge.ts (its owner).

Add an eslint architecture rule (no-utils-upward-import) that forbids
value imports from utils/ back up into a domain directory. Type-only
imports stay exempt: they are erased at compile time and cannot create a
runtime cycle (Settings in modelConfigUtils, CommandContext in
sessionPaths).

No behavior change: typecheck, build, and the affected unit tests pass.

* fix: use Qwen Team 2026 license header on new files (#9146)

* chore: refresh stale utils/ path references after leaf-layer move (#9146)

* docs: reconcile no-utils-upward-import header with the allowed type-only set (#9146)

* fix(cli): allowlist sandbox process.env accesses after leaf-layer move (#9146)

* chore(ci): re-record qwen-autofix.yml size baseline after #9677 (#9146)

#9677 recorded qwen-autofix.yml at 392111 bytes while the file it
committed was already 397656, so every PR that merged main after it
tripped the growth ratchet. Re-record the actual size; the file itself
is unchanged by this PR.

* fix(review): drop the stale utils/findings.ts digest root after the leaf-layer move (#9146)

The #9146 move returned findings.ts to commands/review/, but the digest
root lists merged from main still pinned it under utils/, where the file
no longer exists — the absent root darkened every review's staleness
check and failed review-source-digest.test.ts. Drop the stale file-shaped
root from both digest copies and their pins; the commands/review/
directory root covers the validator at its new home, and the two utils
helpers keep their file-shaped roots.

* fix(review): colocate seatbelt profiles with the sandbox module (#9146)

* fix(review): exempt inline type-only specifiers from the utils upward-import rule (#9146)

* fix(review): report upward inline type-specifier imports under verbatimModuleSyntax (#9146)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(review): pin mixed-specifier and zero-specifier upward imports in the utils rule (#9146)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(review): anchor the nested-checkout utils rule fixture on the last marker (#9146)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(review): pin that the utils/findings.ts digest root stays removed (#9146)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(review): reword stale-bundle SCOPE header to the post-move helper shape (#9146)

* test(review): drop the pre-move utils/findings.ts from the skill-parity fixture (#9146)

* test(serve): derive the seatbelt colocation tripwire from BUILTIN_SEATBELT_PROFILES (#9146)

* fix(architecture): fail closed on computed dynamic imports in the utils leaf rule (#9146)

* fix(cli): point settings.test.ts at the post-move settingsUtils path (#9146)

main updated settings.test.ts after this branch moved settingsUtils.ts
from utils/ into config/, and the merge kept main's old import
specifier, which vite fails to resolve. Repoint it at ./settingsUtils.js;
every other consumer already uses the new path.

* fix(cli): close utils boundary review gaps

* test(cli): cover utils boundary allow paths

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-23 14:41:49 +00:00
Shaojin Wen
57285a94f1
fix(review): repair permissions before giving up on worktree cleanup (#9748)
* fix(review): repair permissions before giving up on worktree cleanup

The review job's end-of-job sweep gave up on the first EACCES and left
foreign-owned leftovers in the shared runner workspace; the next review's
checkout then died on them (run 32577821716, PR #9718: a scratch-verify
tree whose contents this job's user could not unlink, on a pool member
without passwordless sudo). Give the removal a repair ladder — chmod what
this user owns, then passwordless sudo chown/chmod where the pool member
has it, each followed by a retry — and refuse the ladder on paths that
resolve through symlinks, since its sudo leg escalates to root. Members
without sudo still degrade to a named warning: nothing unprivileged can
remove a foreign-owned tree, but the sweep must never fail the job.

Pin the ladder in the cleanup contract test so a rewrite cannot silently
drop it back to warn-and-leave.

* fix(ci): record qwen-code-pr-review.yml's shipped size in the workflow size baseline

The permission-repair ladder added to the review cleanup step (repair
before giving up on a worktree removal, refuse the sudo leg through
symlinks) plus its incident comments grew the file past its recorded
size plus allowance. The growth is the fix itself — the repair logic and
the rationale a future reader needs — not drift, so record the shipped
size rather than trimming the rationale.

* fix(review): pin the repair ladder by effect and enrich its failure warnings

Review feedback on the permission-repair ladder:

- Pin the ladder's effect in the contract test (three removal attempts,
  isolated non-sudo chmod rung, refusal-comparison direction) — the old
  mechanism substrings stayed green when the post-repair retry was
  deleted, when the non-sudo rung was deleted, and when the refusal
  comparison was inverted (all reproduced by mutation before the fix).
- Retry the removal after the chmod rung so a chmod-repaired tree never
  escalates to passwordless sudo; the step comment's "each followed by a
  retry" is now literally true.
- Strip newlines from leftover paths before echoing: leftover names are
  untrusted glob entries, and a fresh line on the runner's stdout would
  parse as a workflow command.
- Both warnings now carry the deciding state: the refusal names the
  branch that fired; the failure warning reports the sudo probe result
  and the survivor's owner.
- Return 0 unconditionally so a failed warning echo can never fail the
  if: always() job via errexit.

* fix(review): close the remaining command-injection entrances in worktree warnings (#9748)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(review): execute remove_review_tree against fixtures and pin its sudo ok-state (#9748)

* test(review): gate the removal-failure fixture on realpath and pin the ladder's guards (#9748)

* test(review): execute the ladder's unpinned arms against behavioral fixtures (#9748)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(review): strip CR and LF from registered-worktree skip warnings (#9748)

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-23 14:27:46 +00:00
顾盼
be891657f7
refactor(node-repl)!: deliver the persistent Node REPL as a standalone MCP server (#9499)
* feat(core): add persistent Node REPL runtime

* fix(core): align Node REPL phase-one contract

* fix(core): align Node REPL module compatibility

* fix(core): remove unused Node REPL host broker

* refactor(node-repl)!: deliver as a standalone MCP server, revert core tools

Replaces the three built-in `packages/core` node_repl tools with a
self-contained MCP server package, `@qwen-code/node-repl-mcp`.

Why
---
Issue #9333 triage accepted the runtime only "for exploration" and gated it
on an open maintainer decision: built-in core tool vs MCP-server-first.
Reversing OpenAI Codex 0.149.0 settled the shape — it ships code mode as a
standalone host with an in-process fallback, and its real-Node `js_repl`
(not the restricted V8 `exec`) is the analogue this roadmap needs, because
stage 3 (#9335) imports cua-driver's N-API addons, which only real Node can
load. Delivering out-of-core also keeps a security-relevant subsystem out of
the maintainer-gated `packages/core` until its value is proven.

What changed
------------
- New `packages/node-repl`: the kernel, module loader, cell transform,
  protocol and kernel manager, ported and now dependency-free (local
  `debug-log`/`win-path`/`tokenizer` replace core utils; `output-adapter`
  emits MCP content blocks in place of `result-converter`).
- Reverted every `packages/core` change, the ~11 packaging files that existed
  only to ship the runtime assets into six distribution layouts, and the two
  design/plan docs describing the core delivery.
- Deleted the trusted-package/sha256 layer: it was empty and unreachable in
  production (`module-loader.mjs` 882 -> 483 lines).
- Wired the package into `scripts/build.js` and the root `vitest.config.ts`.

Net effect: `packages/core` is untouched relative to main; the tool is opt-in
via `mcpServers` instead of registered unconditionally for every user.

Correctness fixes made while porting
------------------------------------
- Stack line numbers were wrong and drifted with binding count (source line 4
  reported as 87). The prelude is now one physical line and the cell compiles
  with `lineOffset: -1`.
- Top-level `var` nested in blocks/try/switch/loops was silently dropped;
  collection now walks the statement subtree, pruning at function boundaries.
  Verified against real Node for 16 constructs.
- A binding named `nodeRepl` permanently broke the output channel; such cells
  are now rejected. Ordinary globals stay shadowable, matching plain Node.
- Errors thrown by imported modules lost their class, `code` and stack.
- A throwing frame handler could discard buffered protocol frames.
- A hoisted `var` assigned before a throw is now kept, as Node does.
- Unhandled rejections settling after a cell no longer vanish.
- Live sandbox timers are capped, so one runaway loop cannot saturate the
  session's event loop.
- Image MIME types are matched case-insensitively on both input paths.
- Binding sort no longer depends on host locale collation.
- The published bin lacked a shebang and mis-detected its entry point, and the
  server plus its kernel leaked on every host disconnect.

Tests: 146 across 14 files, including a compiled N-API addon fixture, 100
consecutive cells, 10 concurrent isolated kernels, stack-line fidelity, and
hoisting semantics. Three smoke scripts cover the adapter, the MCP wire and
process lifecycle; the packed tarball was installed into a clean project and
driven end to end.

Refs #9333

* fix(node-repl): pin zod to the hoisted 3.x so the workspace build type-checks

`packages/node-repl` declared `zod ^4.1.13`, so `npm ci` installed a nested
zod 4.4.3 for it while `@modelcontextprotocol/sdk` resolved the hoisted
zod 3.25.76. Two zod type identities in one compilation made every
`registerTool` input schema unassignable (`Type 'ZodString' is not assignable
to type 'AnySchema'`), failing `npm run build` for this workspace — and with
it the install step of every CI job that builds workspaces.

The SDK accepts `^3.25 || ^4.0`, so pin the hoisted 3.x line and drop the
now-stale nested lockfile entry. One deduped zod, no behaviour change.

This only reproduced with a lockfile-driven install; the local tree had no
nested copy, which is why the build passed locally and failed in CI.

* fix(lint): lint package .mjs node scripts with the node env

The eslint "scripts we run with node" override matched
`packages/*/scripts/**/*.js` but not `.mjs`, nor a package-root `build.mjs`.
`packages/node-repl` is `type: module`, so its `build.mjs` and `scripts/*.mjs`
were linted as browser code and `eslint .` failed with `'process' is not
defined` / `'console' is not defined` / `'setTimeout' is not defined`.

The pre-commit hook only lints `*.{js,jsx,ts,tsx}`, so `.mjs` files are not
checked locally — this only surfaced in the root CI lint step.

Add `packages/*/scripts/**/*.mjs` and `packages/*/build.mjs` to the override,
matching the existing `.js` entries. Generic, additive, no behaviour change.

* fix(node-repl): address round-3 review findings (resolution, error identity, image bound)

Triaged the bot's round-3 critical findings against the ported code and fixed
the three that were genuine defects here; each has a regression test.

- R3-5 (resolution): a symlinked `<cwd>/node_modules` — the norm under pnpm,
  monorepo hoisting and shared CI caches — was silently dropped, so the
  documented zero-config `await import('pkg')` failed with "cannot resolve from
  0 module roots" while plain Node resolved it. The implicit cwd root was
  applying a re-link self-comparison that only makes sense for registered roots
  (which carry a registration-time canonical baseline). Follow the symlink for
  the implicit root, but skip it entirely when the cwd node_modules is itself a
  registered root, so the registered root's revocation guard is not undermined.

- R3-3 (error identity): an error thrown by a host builtin inside imported code
  (e.g. `fs.readFileSync` → ENOENT) was rewrapped message-only, dropping
  `code`/`errno`/`syscall`/`cause`/`stack` — so `catch (e) { if (e.code ===
  'ENOENT') }`, a ubiquitous Node idiom, silently took the wrong branch. Carry
  those fields onto the realm-wrapped error.

- R3-6 (image bound): image-frame `mimeType` was unbounded — only `data.length`
  counted against the raw image budget — so a malformed/forged frame could
  retain a huge string that also got interpolated into a notice and tokenized.
  Bound the MIME at frame ingestion (a real image MIME is a few dozen chars).

- R3-4 is by design (the runtime is not a security boundary), but the tool
  description recommended `createRequire` without noting it is not subject to
  the process denial or module-root containment the import path enforces; the
  description now says so.

- R3-1 / R3-2 (the `.mjs` lint failure) were already fixed in an earlier commit.

Suite: 148 tests (added symlinked-cwd resolution and host-builtin error-code
regressions). The symlink fix initially defeated the registered-root revocation
test; the registered-root deferral above resolves both.

* docs(node-repl): note top-level function/class re-declaration needs a fresh name

Round-3 review R3-12/R3-33: re-declaring an existing top-level function or class
in a later cell is a link-time SyntaxError (they persist as `let`, which the
prelude re-declares), whereas a plain Node REPL accepts it. A correct fix needs
declaration-kind tracking the manager does not carry today; until then the tool
description's rerun guidance ('prefer var') is corrected, since a function cannot
be made rerunnable via var.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-23 14:20:39 +00:00
顾盼
b5fbdb22d3
feat(cua-driver): add versioned Computer Use SDK and release pipeline (#9587)
* chore(cua-driver): sync upstream v0.20.0

* feat(cua-driver): add Computer Use SDK with versioned observation revisions

Wrap the typed driver SDK in a standalone Node wrapper and add accessibility.observation_revision.v1: base-anchored validated diffs with opaque element tokens, explicit full-resync reasons, full-only answers on Windows/Linux. Fix portable include_screenshot schema type and classify stable kAXErrorFailure refusals as complete in the macOS capture tracker.

* feat(cua-driver): complete typed Computer Use capabilities

* fix(cua-driver): use Qwen-owned npm identity

* feat(cua-driver): publish one Qwen CUA SDK package

* fix(cua-driver): verify Windows Rust targets

* fix(cua-driver): verify macOS Rust targets

* fix(cua-driver): pin release Rust toolchain

* fix(cua-sdk): fail closed on incomplete releases

* test(cua-sdk): import workflow test globals

* ci(cua-sdk): retry Debian package downloads

* fix(cua-driver): harden lifecycle and release gates

---------

Co-authored-by: tutu <tutu@U-RD4R9MQQ-2235.local>
2026-08-23 14:20:14 +00:00
qqqys
acc46e58cb
fix(autofix): give the repair pass a budget it can finish in (#9691)
* fix(autofix): give the repair pass a budget it can finish in

The repair attempt ran on a hardcoded 18-minute agent budget while the
primary attempt gets 120 minutes from a configurable default. Raise the
repair budget to 45 minutes and carry the step and job caps that bound it.

The repair attempt is handed strictly less to work with than the primary
one: a deterministic rejection is an opaque check failure, not the
structured review feedback the primary attempt receives, so it must first
re-derive which change caused the rejection before it can amend anything.
Giving that 15% of the primary budget inverted the difficulty and the
allowance.

Measured on four takeover PRs over nine rounds on 2026-08-21: the primary
attempt reported `Autofix agent completed address-review successfully.` in
9 of 9 rounds, and the repair attempt hit `timeout (1080000ms)` in 9 of 9.
Every one of those rounds discarded work the primary attempt had already
finished — on #9340 a completed `origin/main` conflict resolution across
three files with two mutation probes and `vitest run src/commands/review/`
green at 97 files / 4335 tests. Three such rounds tripped
TIMEOUT_WINDOW_CAP and parked the PR at its round cap with
`autofix/needs-human`.

The rejections themselves were a mix — a flaky unrelated test (#9648), a
genuine defect in the PR, and a scope violation — so this is not a
substitute for fixing any one of them. It is the step they all funnel
through: whatever the gate rejects on, the repair attempt has to be able
to finish before the round can push.

Carried bounds, each preserving its documented margin:

- repair step cap 20m → 55m (budget + the same 10-minute margin the
  primary attempt keeps, so the internal kill path still writes
  `agent-timeout` before the step cap fires)
- review-address job cap 300m → 330m (the four long steps now sum to 305m
  plus the 25m setup/report reserve)
- PENDING_STALE_MIN 330 → 360 (its 30-minute margin over the job cap, so a
  live review-address run is never aged out mid-flight)

45 minutes is deliberately a fraction of the primary budget: a repair that
cannot land in 45m is a handoff, not a longer retry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VgTjRF91xANQh6SY9YGyCf

* fix(autofix): carry the raised repair bounds through sibling prose

* fix(autofix): revert design-record edits outside this PR's footprint (#9691)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.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>
2026-08-23 00:32:36 +00:00
qqqys
2a57f86198
fix(ci): gate the fork signal on fields the review payload delivers (#9469)
* fix(ci): gate the fork signal on fields the review payload delivers

`qwen-autofix-fork-signal.yml` gated on
`github.event.pull_request.maintainer_can_modify == true`. That field does
not exist in a `pull_request_review` payload: the event carries the SIMPLE
pull-request object, and `maintainer_can_modify` — like `mergeable`,
`additions`, and `changed_files` — ships only on the full object the
`pull_request` event sends. The expression evaluated to null on every
delivery, `null == true` is false, and the job's `if` could never hold.

Measured on the repository: across the 300 runs between the bridge shipping
(#8676, 2026-08-07) and this change, 290 skipped, 7 cancelled, 1
action_required, and 0 success. Not one signal ever reached its step, so the
bridge behind it has never fired either — every fork-PR review has been
served by the scheduled scan alone, which is exactly the throttled backstop
this bridge exists to get ahead of.

The consent check is not lost, and does not move: the bridge already re-reads
it live (`gh pr view --json maintainerCanModify`, then
`select(… .maintainerCanModify == true)`), and that read was always the
authoritative one — consent can be withdrawn between the review and the
dispatch, so a payload copy could only ever have been a stale early-out. The
signal job cannot make that call itself: it holds `permissions: {}`, no
secrets and no checkout, deliberately, because it runs on a fork-triggered
event.

What the removal does cost is one signal + bridge run and one PR read for a
takeover-labeled fork PR whose author has turned maintainer edits off, where
the gate previously intended to spend nothing. Ordinary contributor fork PRs
are unaffected — the bot-authored-or-takeover-labeled conjunct still excludes
them.

Also adds a regression test asserting the gate references no full-object-only
field. A gate like this fails silently: the job's entire body is one echo, so
"never opens" and "no fork review happened to qualify" look identical from
outside, which is why this went twelve days unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(ci): match the full-object-only guard on a word boundary (R1-1)

The guard asserted the signal gate references no full-object-only field via
bare substring containment, but four deny-list entries are strict prefixes of
fields the simple pull-request object DOES deliver: `merged` in `merged_at`,
`commits` in `commits_url`, `comments` in `comments_url`, `review_comments` in
`review_comments_url`. A future edit adding a legal conjunct such as
`github.event.pull_request.merged_at == null` would turn the suite red with a
message blaming a full-object-only field — pushing the author to drop the
conjunct or weaken the guard itself.

Anchor each check on a word boundary, and add a test that pins the matcher's
discrimination on all four prefix pairs so the substring form cannot come back
unnoticed.

* test(ci): see full-object fields through the index operator (R2-1)

The full-object-only guard matched `pull_request.<field>` literally, so it
only saw the `.` property de-reference. GitHub Actions reaches the same
property through the documented `[ ]` index operator, on any segment of the
path, and `github.event.pull_request['maintainer_can_modify'] == true`
evaluates exactly as the dot form did: the field is absent from the simple
`pull_request_review` payload, `null == true` is false, and the gate never
opens. The guard stayed green through it — a silent replay of the incident
this PR fixes, invisible to the test written to catch it.

Rewrite the index form to the dot form before matching rather than
enumerating spellings, so one matcher covers every combination of the two at
any depth. The word-boundary anchor from R1-1 is unchanged, so the four
delivered fields the deny-list names prefix (`merged_at`, `commits_url`,
`comments_url`, `review_comments_url`) still pass in every spelling.

A `fromJSON(toJSON(github.event.pull_request))` round-trip still evades this;
no textual guard catches that one, and the comment says so.

Mutation-verified, each mutant reddening the tests that pin it:

| mutant | result |
|---|---|
| normalization removed (identity) | 2 failed — bracket and mixed spellings go unseen |
| word boundary dropped | 1 failed — `merged_at` rejected as `merged` |

`npx vitest run --config ./scripts/tests/vitest.config.ts
scripts/tests/qwen-autofix-fork-bridge-workflow.test.js` -> 12 passed (12).
eslint and prettier clean.

* test(ci): pin the fork-signal guard's bracket-whitespace tolerance

R3-1: `asDotAccess` rewrites `pull_request['field']` to the dot form before
matching, and its regex deliberately tolerates whitespace inside the index
(`\[\s*…\s*\]`) because GitHub Actions accepts
`github.event.pull_request[ 'maintainer_can_modify' ]` as a legal expression.
No spelling in `referenceSpellings` carried that whitespace, so the tolerance
was unpinned: deleting both `\s*` left all 12 tests green, and a later gate
edit written in the spaced form would have reached the same absent field and
restored the always-false gate this PR fixes.

Add the spaced-bracket spelling, which both index-operator tests consume.

Mutation-verified: with the two `\s*` deleted from `asDotAccess`, this file
now fails 2 tests ("rejects a full-object field without rejecting the fields
it prefixes" and "sees a full-object field through the index operator");
before this commit the same mutation left 12 passed.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 18:47:00 +00:00
Shaojin Wen
a5d77eb817
chore(ci): migrate autofix prose to the design record and ratchet growth (#9677)
* chore(ci): migrate autofix prose to the design record and ratchet growth

`qwen-autofix.yml` was at 462,720 bytes — 90% of GitHub's 500 KB
start-runs limit and 7,280 under this repo's gate. The prose migration in
#9517 had regained 78 KB; a single feature commit gave 25 KB of it back
two days later, 53% of it comment lines that belonged in the design
record. Nothing objected, because the gate is a ceiling and only speaks
when a file is already at the wall.

Two changes, addressing the level and the slope.

Level: 76 comment blocks of 8+ lines move into qwen-autofix.md as
af-073..af-148, each leaving its opening two lines plus the usual
pointer. The file drops to 419,995 — 50 KB under the gate.

Slope: every workflow's recorded size now lives in
.github/workflows/.size-baseline, and check-workflow-size.sh fails a file
that exceeds its entry by more than 4 KB. Growing a file is still
allowed; the ratchet only insists the growth be visible in review instead
of discovered at the wall. A file well under its baseline warns so the
slack is reclaimed rather than banked.

Two things the migration had to learn, both caught by contract tests
rather than by inspection:

Identical prose shares one af id. A step inlined into several jobs must
stay byte-identical across copies, and minting a separate id per copy
broke that for the git-config sanitize step — same length, different
pointer digits.

A cross-file editing contract is not prose. "This copy and the one in
qwen-triage must be edited together" only does its job where the editor
will see it; moving it to the design record is how a lockstep silently
breaks. Blocks naming a sibling workflow or script, or spelling the
contract out, stay put.

Behaviour is unchanged and checked, not assumed: both YAML documents were
parsed and compared with comment lines dropped from every string, and
they are equal.

* fix(ci): harden the workflow-size ratchet per review

- key the vitest baseline lookups by separator-agnostic file name so the
  merge-queue Windows lane resolves entries (split('/') missed win32 joins)
- fail closed on malformed .size-baseline lines: non-numeric values,
  leading zeros (bash octal), and extra fields no longer disable or
  mis-key the ratchet; keep an unterminated final line
- execute the gate script end-to-end in tests (growth, missing entry,
  missing baseline, slack warning, malformed line) so its decision
  branches are witnessed
- correct af-079/af-123/af-084 attributions in the design record

* fix(ci): pin ratchet boundaries and skip pre-bash-4 test hosts

* fix(ci): end migrated autofix teasers at sentence boundaries per review

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-22 16:31:56 +00:00
Shaojin Wen
ea872a4621
fix(review): stop the fallback comment from firing on superseded runs (#9716)
* fix(review): stop the fallback comment from firing on superseded runs

The fallback-comment gate admitted every review-pr result of 'cancelled'
on the assumption that a run-level cancel takes the queued fallback job
down with it, leaving job-level timeout as the only live source of a
cancelled review step. That assumption does not hold: the job is guarded
by always(), which keeps it running through a run-level cancel. On
PR #9131 a same-head pull_request_target pair started 1s apart, the
newer run cancelled the older inside authorize, and the older run posted
a false "review did not complete" (run 32558544379) while the surviving
run was still reviewing — same head, so the in-step head-moved guard
could not catch it.

The two cancels are separable through needs: a job-level timeout cancels
review-pr alone while its upstream jobs completed long before, whereas a
run-level cancel sweeps the whole chain. 'cancelled' now opens the gate
only when neither authorize nor delay-automatic-review was itself
cancelled.

* test(review): pin the fallback gate's upstream-cancel conjuncts

Review R1-1: the resilience suite's only cancelled-related assertion —
toContain("needs.review-pr.result == 'cancelled'") — matched the old and
the new gate alike, so reverting the two != 'cancelled' conjuncts would
bring back the #9131 false-fallback regression with the suite green.

Pin the full compound clause, grouping included, so dropping either
conjunct (or the parenthesization) fails the test, and rewrite the
rationale comment that still asserted the falsified premise that a
run-level cancel takes the queued fallback job down with it.

Mutation-verified: the gate test fails on a reverted-gate mutant and
passes on the PR gate.

* test(review): assert the bare cancelled disjunct is absent from the gate

Review R2-1: the compound-clause pin proves presence, not absence — a
merge-conflict resolution keeping both sides of the gate hunk re-adds
the bare "== 'cancelled' ||" disjunct beside the intact compound
clause, reopening the gate on every cancelled review-pr with the suite
green. Add the negative assertion; inside the compound clause the
substring is followed by ' &&', so it holds on the intended gate.

Also take the round-2 deferred note: the job comment now says a
run-level cancel landing after the upstream chain finished still opens
the gate and is suppressed by the in-step head-moved guard, and why a
same-head twin cannot land that late.

Mutation-verified: with the bare disjunct re-added the gate test fails
on the negation; the pristine workflow passes.

* test(review): pin the cancelled-check count; credit the PR-state check

Review R3-2: the negative assertion rejected only one rendering of the
bare disjunct — a parenthesized or respaced re-addition beside the
intact compound clause escaped both pins with the gate reopened on
every cancelled review-pr. Replace the negation with an
occurrence-count pin: exactly one "needs.review-pr.result ==
'cancelled'" in the gate catches any rendering, while the compound
pin keeps guarding the conjuncts.

Review R3-1: the rationale comment's list of late run-level cancels
missed the closed-action flavor — a same-head cancel hours in with the
upstream chain green, stopped only by the in-step PR-state check the
comment never credited. Credit it.

Mutation-verified: bare, parenthesized, and reverted-conjunct gate
mutants each fail the gate test; the pristine workflow passes at base
parity.
2026-08-22 15:38:34 +00:00
易良
10c647f320
refactor(core): remove root barrel self-imports and enforce the boundary (#9635)
* refactor(core): remove root barrel self-imports

* fix(core): activate root barrel boundary rule

* test(core): cover root barrel export visitors

* fix(core): remove unused boundary constant

* fix(core): correct misplaced imports flagged by root barrel rule

coreToolScheduler: ToolExecutionStatus is exported from core/turn.ts, not
tools/tools.ts; was also duplicated in the tools import block.
nonInteractiveToolExecutor: RuntimeContentGeneratorView is exported from
agents/runtime/agent-context.ts, not core/contentGenerator.ts.

Both were surfaced by the new no-core-root-barrel-import rule in #9152.

* fix(core): complete telemetry mock and harden barrel rule edge cases

extensionManager.test: the ../telemetry/loggers.js mock was missing
logExtensionInstallEvent, logExtensionUninstall, and logExtensionDisable,
which the source now imports from that path after the barrel cleanup.
This caused 53 test failures.

no-core-root-barrel-import.js: added package-specifier detection
(@qwen-code/qwen-code-core), template-literal dynamic import support,
depth-0 ./index.js handling, and __tests__ exemption parity with the
test-file check. Added trailing newline.

Tests: added coverage for all new edge cases in both rule test files.

Refs #9152

* fix(core): close package-level barrel gap and coverage holes

The barrel rule blocked only specifiers resolving to the src-level
index.{js,ts}; the package-level barrel (packages/core/index.ts, which
re-exports the src barrel) remained reachable via one extra ../ hop.
Match ../index.{js,ts} so core production files cannot re-import the
root barrel through that path.

Also: hoist the duplicated src-root marker to a single constant, reword
the rule message so it no longer hardcodes ../index.js, pin the .ts
spelling branch, fix the vacuous fixtures-exemption case, and add a
cross-directory flat-config fixture so a narrowed files glob fails the
integration test.

Refs #9152

* fix(core): flag barrel subpaths and inline type imports in the root barrel rule

* fix(core): harden no-core-root-barrel-import rule

Anchor the source-root marker on its last occurrence so nested checkouts resolve correctly, reject relative imports of the compiled dist barrel, and treat .spec files as tests. Adds regression coverage for each.
2026-08-22 13:42:10 +00:00
易良
b455bad5e5
refactor(cli): keep acp-integration off serve internals (#8084) (#9144)
* refactor(cli): keep acp-integration off serve internals (#8084)

The dependency direction set in #8084 regressed: native Live Voice
(a5c637b749) added four acp-integration imports of serve/live modules,
because nothing in the repo enforces the boundary the issue defines.

Ownership, measured by consumer rather than by directory:

- capture-screen-context, live-task-tools, live-speak-to-user and
  live-backend-instructions each have exactly one production consumer,
  acp-integration/session/Session.ts, and import nothing from serve/.
  They move to acp-integration/live/ with their tests.
- conversations/session-source is shared by acpAgent and four serve
  modules, has no imports, and takes its reader as a parameter, so it
  moves to runtime/live-session-source.ts alongside the other neutral
  contracts. Renamed because every symbol in it is Live-specific.

Adds a no-restricted-imports rule for acp-integration/** so the next
feature spanning both surfaces gets a lint error pointing at runtime/,
rather than silently reopening the criterion.

No behavior change: moves, import rewrites, and the lint block.

* fix(cli): harden the acp/serve boundary guard (round 2)

- Flag the bare '../serve' directory specifier, which resolves to the
  serve/ barrel and skipped the trailing-segment group patterns (also
  added to the utils/ guard for symmetry).
- Extend the same boundary to runtime/, the layer the rule directs
  authors to, so the #8084 coupling cannot reform one hop away.
- Cover dynamic imports: no-restricted-imports never visits
  ImportExpression, so a no-restricted-syntax selector now enforces the
  boundary for await import('../serve/...') too. The acp-integration
  block moves after the general TS block (flat config lets the last
  matching block win per rule) and restates its no-restricted-syntax
  selectors so the override drops nothing.
- Document that CI lint is the enforcement point; no fixture test pins
  the block.

Verified: synthetic fixtures for all three violation shapes are
rejected; full npm run lint passes with no live violations.

* test(cli): pin serve boundary lint rules

* test(cli): close serve boundary lint gaps

* fix(lint): close serve-boundary entrances and harden the guard

- reject computed dynamic-import sources (concatenation, new URL) and
  type-level imports fail-closed; rounds 2-5 each demonstrated a new
  per-spelling regex entrance, so non-literal forms are blocked outright
  (R4-1)
- rewrite the boundary patterns without nested quantifiers; the previous
  shape backtracked exponentially (~4x per two ../ segments, lint-time
  ReDoS) (R5-2)
- build the three guarded override blocks no-restricted-syntax arrays from
  one shared helper so flat config last-wins cannot silently drop selectors
  (R5-3)
- pin the bare-directory barrel specifier in fixtures (R5-4) and add a
  string-throw probe pinning the restated selectors in the overrides (R5-5)
- replace the **/serve* static globs with enumerated relative depths so
  third-party serve-named packages are never flagged (R5-7)

* fix(lint): correct TSImportType selector path and computed-template handling

- read the type-import specifier at argument.literal.value: @typescript-eslint
  wraps it in a TSLiteralType, so argument.value was dead code and the old
  fail-closed TSImportType selector over-matched every type-level import
  (37 errors in files this PR never touches) (round-6 Critical)
- reject computed template literals (templates containing expressions)
  fail-closed; pure-literal templates stay covered by the quasis pattern
  selectors — the old blanket TemplateLiteral exemption contradicted the
  fail-closed comment above it (round-6 Critical)
- give the fail-closed selectors a distinct message: computed sources
  cannot be checked against the boundary, which is not the same policy as
  importing serve/ (round-6 suggestion)
- pin the depth-enumeration loop beyond depth 1 with a depth-2 fixture,
  pin the fixed type-import selector with a negative typeof-import control,
  and pin the computed-template fail-closed path (round-6 suggestion)

* fix(lint): close the remaining round-6 serve-boundary entrances

Complements the previous commit (which fixed the TSImportType selector
path and computed-template fail-closed) with the R4-1 entrances it left
open, each pinned by a fixture:

- percent-encoded segments (`../%73erve/index.js`): Node percent-decodes
  segments when mapping the resolved URL to the filesystem, so raw-text
  patterns cannot see through them — any `%` in a guarded-tree specifier
  is now rejected with a dedicated message.
- static traversal twins: the pattern regexes now run over static
  ImportDeclaration/ExportNamedDeclaration/ExportAllDeclaration sources
  too, closing `import './../serve/x'`, `import '../runtime/../serve/x'`,
  and `import '..//serve/x'`, whose dynamic twins were already blocked.
- leading literal segment: a traversal-anywhere pattern catches
  `import('foo/../../../serve/x')` past the dot-slash anchor.
- vitest module-loading calls (vi.mock/doMock/importActual/importMock)
  resolve and load the real module, so they get the same patterns plus
  fail-closed coverage for computed arguments.

* fix(lint): cover vitest serve-boundary calls

* fix(lint): close the round-7 serve-boundary entrance classes

R4-1 round-7 interim hardening (the durable specifier-resolving custom
rule remains tracked separately):

- case-variant spellings (../Serve/...): every pattern, percent and
  quasis attribute regex now carries the i flag, covering the dynamic,
  static, vi.*/vitest.* and TSImportType arms.
- ?query/#fragment suffixes: rejected alongside % in all eight
  specifier shapes (bundlers/Node strip them when resolving, so
  '../serve?x' reaches the same module as '../serve').
- percent-encoded pure-template vitest calls: added the missing
  arguments.0.quasis.0.value.cooked twin to the reject list.
- root-absolute and file: literal specifiers: fail-closed rejected in
  every literal shape (guarded trees sweep verified clean of both).
- createRequire: its source modules ('module'/'node:module') are
  flagged in guarded trees, since the alias escapes the
  callee-name="require" arm and Node >=22 require(esm) loads serve/.

Each entrance class is pinned by a fixture case (18/18 green through
the real ESLint API); the three guarded trees lint clean with the new
arms.

* refactor(lint): resolve the serve boundary by resolution, not text (#8084)

R4-1 round-8 decision (maintainer-approved option a): replace the
spelling-by-spelling regex/glob matrix with a local resolution-based
ESLint rule (eslint-rules/no-serve-boundary-cross.js).

Eight review rounds each demonstrated a new spelling escaping the text
matrix (data: URLs, percent-encoding, traversal through a leading literal
segment, baseUrl bare specifiers, createRequire/getBuiltinModule,
TSImportType, aliased vitest loaders, Worker/fork), because every spelling
is just another way to NAME the same target. The new rule resolves each
import-like specifier against the importing file and reports anything
landing inside packages/cli/src/serve/:

- relative specifiers resolved against the importing file
- baseUrl bare specifiers resolved against packages/cli (tsconfig baseUrl
  makes `src/serve/...` reachable — the round-8 entrance text never saw)
- file: URLs resolved to concrete paths (case-insensitive, whitespace-trimmed
  scheme detection, since the URL parser normalizes both)
- vitest loaders matched alias-proof (v.mock / destructured importActual);
  only specifiers resolving INTO serve/ report
- child_process.fork checked; spawn deliberately not (first arg is an
  executable, not a module)
- fail-closed on statically-unresolvable sources: computed sources, data:
  URLs, traversal-bearing bare specifiers, node:module imports,
  process.getBuiltinModule
- case-insensitive path comparison (Serve/ loads serve/ on
  case-insensitive filesystems)

Fixture suite reworked to resolution semantics: several round-4..7 fixture
depths corrected to spellings that genuinely resolve into src/serve (the
old depths resolved to packages/cli/serve, outside src/serve, and were
only caught by text matching); new pins for every round-8 entrance and for
the Codex self-review Criticals (aliased loaders, uppercase/whitespace URL
schemes, spawn not an import source). 26/26 pass; guarded trees and the
full cli src lint clean (zero false positives).

Removed: relativeServeImportPatterns, restrictedServeImports,
serveDynamicImportPatterns, serveGuardSyntaxRules and the per-spelling
selector/percent/absolute/createRequire special cases.

* fix(lint): drop the dead serveGuardSyntaxRules helper

The resolution-rule commit removed the mechanism but left the
serveGuardSyntaxRules helper behind — unused (no-unused-vars) and
referencing the already-deleted restrictedServeDynamicImports (no-undef),
which failed CI's repo-wide eslint. The guarded trees inherit
restrictedRequire + restrictedStringThrow from the general TS block, so
nothing is lost.

* fix(lint): close serve boundary resolver gaps

* fix(lint): address serve boundary review suggestions

- R9-2: move the new-URL-with-import.meta check into the NewExpression
  visitor with the real MemberExpression base shape; the CallExpression
  placement was unreachable and standalone new URL(...) reported nothing
- R9-3: report module-builtin entrances via the moduleBuiltin messageId
  instead of the self-contradicting failClosed remediation text
- R9-4: match the destructured fork(...) spelling, not just
  child_process.fork(...)
- R9-5/R8-2: fixture pins for re-exports, Worker, fork, require,
  vi.doMock and vi.importMock entrances
- R9-7: pin the false branch of static-template concatenation (pure
  template literals resolving outside serve stay allowed)
- R10-3: filter the third-party serve-named package pin by ruleId so a
  failClosed false positive also turns it red
- R13-2: pin resolution detections on the serveBoundary messageId so
  inside-detection degrading to blanket fail-closed cannot ship green

* fix(lint): close round-11 serve boundary gaps

Critical fixes:
- '#name' package-imports specifiers sailed through: stripUrlSuffixes
  splits on '#' before the fail-closed check saw it, so the branch was
  dead code and '#s' classified outside. Check '#' before suffix
  stripping (fixture pins both entrances).
- scheme detection used JS trim(), which keeps non-whitespace C0
  controls — '\x01data:…' slipped past while Node's URL parser strips
  C0-or-space at the edges and loaded it. Detect schemes on the
  WHATWG-normalized form (fixtures added).
- eval("(0,eval)"/globalThis.eval spellings included) and new Function
  can embed import('…') the rule cannot resolve — fail closed like
  computed sources; no-eval/no-new-func are not enabled in the shared
  config and the guarded trees contain no such calls.
- backslashes normalize to '/' under Node's URL-based ESM resolution
  (file: URLs are special), so '..\\serve\\x.js' loaded serve/ on
  posix while the rule saw a bare specifier. Normalize backslashes
  before classification (fixture added).

Hardening + pins:
- fork/Worker arms match object-agnostically (namespace/default-import
  spellings no longer evade); Worker skips new URL(spec, import.meta.url)
  arguments so the URL arm owns them (no more fail-closed false positive
  on the canonical construct, no double report on serve targets).
- new TSImportEqualsDeclaration visitor: import x = require('../serve/…')
  emits a working createRequire shim under tsc NodeNext.
- isProcessObject accepts computed properties (globalThis['process']) and
  the Reflect.apply arm accepts computed getBuiltinModule; the three
  getBuiltinModule arms collapse into one via a shared property matcher.
- vitest loader names lifted into a module-level constant; corrected two
  stale comments (fail-closed branches; R5-5 probe description).
- fixtures: root-absolute/file: inside verdicts (repoRoot, messageId),
  fork member arm, template cooked values, bare vitest loaders, dynamic
  bare-'module' entrances, outside-serve negatives for URL/Worker/fork/
  require. Suite 44/44.

* fix(cli): restore live session source import

* fix(lint): clear the two lint errors breaking CI on the boundary rule

Follow-up to the round-11 batch, which landed without running the
repo lint:
- the C0-edge-strip regex legitimately contains control-character
  ranges (it mirrors the WHATWG URL parser), so disable
  no-control-regex on that line with a rationale comment instead of
  rewriting the range.
- drop the unused UTILS_FIXTURE constant from the boundary tests
  (no fixture lints a utils/ file).

eslint clean on both files, boundary suite 44/44, prettier clean.

* fix(lint): close bounded serve boundary gaps

* fix(lint): complete the round-12 boundary escape closures

Extends the previous commit (which canonicalized the serve/baseUrl
comparison sides, added staticMemberPropertyName, and closed the
Function-call and Worker-eval-option shapes) with the remaining
round-12 review surface — every demonstrated spelling probed before
and after:

- Callee identity is now shape-tolerant end to end: rightmost-segment
  object matching (nested member objects like globalThis.vi / x.cp no
  longer evade the object-agnostic arms), renamed loader bindings
  resolved from the import declarations (fork-as-f, Worker-as-W),
  Reflect.apply/construct unwrapped for guarded targets (fork included),
  Function.prototype.call/apply/bind indirection handled (.call unwraps
  with shifted args; .apply/.bind fail closed), and the
  SequenceExpression unwrap applied uniformly instead of eval-only.
- The string-code execution class fails closed beyond Function/eval
  direct calls: .constructor property chains (({}).constructor.constructor,
  (function(){}).constructor, AsyncFunction variants), eval.call/apply,
  and the node:vm surface (runInThisContext / runInNewContext /
  runInContext / compileFunction / new vm.Script, scoped to vm imports).
- The Worker eval option fails closed unless eval is statically false
  (a dynamic option or non-object second argument is unverifiable), and
  the URL arm's import.meta base restriction reports failClosed on the
  construct.
- Fixtures pin each class: shape variants, renamed bindings, Reflect
  indirection, call/apply/bind, the string-code family (incl. messageId-
  specific failClosed pins for the eval:true Worker and non-url
  import.meta bases), bare-directory/baseUrl query-suffix spellings, and
  outside-serve allow pins for the export/import-equals arms.
- expectServeBoundaryError now filters on the rule id (all three
  messageIds contain 'serve'); the divergent substring negative pins
  move to expectNoBoundaryHits.

Suite 52/52; guarded trees lint clean (no false positives from the new
arms); eslint + prettier clean.

* fix(lint): close the round-12 reviewer escape classes (#8084)

Ten Criticals plus five hardenings from the round-12 review, every
class probe-verified before and after:

- R12-1: Worker eval-option analysis now matches runtime object-literal
  semantics — the LAST eval key wins (duplicates included), an options
  object without eval defaults to false (specifier path, no
  over-block), and a spread after the last literal eval is
  unverifiable — fail closed.
- R12-2: sequence unwrapping is now a uniform invariant — recursive on
  callees in both visitors and applied to object expressions
  (rightmostObjectName/isProcessObject), closing (0, require).call,
  (0, (0, require)), (0, process).getBuiltinModule, (0, vm).* and
  new (0, vm).Script.
- R12-3: call/apply/bind indirection is complete — Function/constructor
  forward code (unconditional fail-closed), chained indirection
  (x.call.call) fails closed instead of falling through, and vm exec
  names plus the fork/vitest alias sets resolve.
- R12-4: Reflect.apply/construct target lists mirror the direct-call
  arms — Function (incl. member spellings), the vm exec/Script surface,
  and the vitest loaders (member and identifier targets).
- R12-5: alias sets populate in a pre-pass over the module body — ESM
  imports are hoisted, so use-before-import now resolves like the
  import-first direction.
- R12-6: renamed destructured vitest imports resolve through a new
  vitestLoaderAliases set.
- R12-7: a named guarded global (process/globalThis/global) carrying an
  opaque computed key fails closed (process-family keeps the dedicated
  moduleBuiltin message); object-agnostic arms keep their documented
  residue.
- R12-8: .constructor fails closed on variable bodies and expression
  templates; statically non-string literals keep the pass-through.
- R12-9: inline lazy vm imports ((await import('node:vm')).*) count as
  vm objects in the exec and Script arms.
- R12-11/12/13: checkSource skips statically non-specifier arguments
  (no unactionable advice for env objects), the URL arm owns
  new URL(spec, import.meta.url) on every entrance (no over-block, one
  report on the serve form), and the module builtin reports
  moduleBuiltin on every entrance.

Test hardening: R12-10 normalizes the repoRoot pin to forward slashes
(Windows merge-gate determinism), R12-14 pins the four mutation
survivors, R12-15 pre-cleans and catch-cleans the baseUrl symlink
links. Suite 65/65; guarded trees lint clean.

* fix(lint): repair corrupted files from the git-API blob upload

The previous commit (552bc7c8f) was pushed via the GitHub git API with
`-f content=@file` blob payloads that GitHub stored corrupted (9-byte
binary blobs), breaking the eslint config load (SyntaxError) and the CI
Test gate. Re-upload both files with JSON --input payloads whose blob
SHAs match the local git objects byte-for-byte (8270a0d2 / 5a0ebaa2).
No content change beyond restoring the intended files; suite 65/65.

* fix(lint): close the round-13 serve-boundary escape classes (#8084)

- re-normalize backslashes AFTER percent-decoding so %5c/%5C cannot
  reintroduce a traversal the pre-decode normalization missed
- on realpath ENOENT canonicalize the deepest existing ancestor and
  re-append the missing tail, so symlinked-ancestor checkouts fail
  closed instead of open
- Worker eval-option scan: treat an unresolvable computed key like a
  spread (unknown), fail closed on a non-computed __proto__ prototype
  unless it is statically null, and resolve quoted string-literal keys
- carry the opaque-key fail-closed check through composed callees:
  one hop below .call/.apply/.bind, as a Reflect target, and on the
  getBuiltinModule object side, with the process-family message

Pins all four classes with executed fixtures plus negative controls;
the guarded trees stay lint-clean.

* fix(lint): close the round-13 binding-hop and callee-opacity escapes (#8084)

* refactor(cli): simplify ACP serve boundary guard

* fix(lint): close the round-21 contract pins and bare-barrel escape (#8084)

* fix(lint): close dynamic-import and js-file holes in the acp/serve guard (#8084)

* fix(lint): make acp/serve dynamic-import guard case-insensitive

The no-restricted-syntax selector for dynamic import() of serve/ was case-sensitive, so a macOS case-variant specifier (`../Serve/...`) would resolve to the daemon barrel without tripping the guard. Add the /i flag and cover case-variant plus computed-specifier behavior.

---------

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-08-22 12:39:08 +00:00
Shaojin Wen
5efb42458a
fix(autofix): pass CI=true through the gate's env -i launches (#9649)
* fix(autofix): pass CI=true through the gate's env -i launches

The verification gate launches (first pass + repair pass) run the branch's
build/typecheck/lint/test through an env -i clean child that allowlisted
only 8 variables and dropped the runner-provided CI=true. Without it the
gate's checks run with inverted CI semantics relative to the repo's
regular CI: packages/cli/src/ui/auth/AuthDialog.test.tsx skips 18
TUI-input tests on CI as unreliable, and without CI=true they un-skip
inside the gate and one flakes (~5s vi.waitFor) — reject_fix fires
retryable on a fix the PR's own CI passes green, burning the repair pass
and mislabeling the round's A/B baseline.

Add CI="${CI:-true}" to both env -i allowlists (probe: CI=true → file
green; env -u CI → the TUI test fails 1/25), and pin the full allowlist
contents in the contract tests — the old pin counted env -i occurrences
only, so a missing variable shipped green.

Follow-up from PR #9262 (R5-1); issue #9648.

* fix(autofix): widen gate allowlist pins to lowercase env names

The allowlist pins extract passed variables with [A-Z_][A-Z0-9_]*, so a
lowercase or mixed-case entry — e.g. npm's own npm_config_* convention
— is invisible to the sorted-multiset check: adding one to a single
launch ships green, and only a later asymmetric drop then fails,
producing exactly the divergent-environment regression the pins exist
to catch while CI stayed green the whole way.

Widen the name class to [A-Za-z_][A-Za-z0-9_]* in both pins — the gate
launches pin added in 8db672e80c and the sibling run_deferred_upsert
pin that shares the identical regex and blind spot (probe: inject
npm_config_registry="..." into one launch → old regex 215/215 green,
widened regex fails with + "npm_config_registry" at each pin; pristine
workflow stays green).

Review round 1 finding R1-1.

* fix(autofix): pin the gate clean-child launches structurally (#9649)

* fix(autofix): pin the gate run-body statement list around the launch (#9649)

* fix(autofix): pin gate startup channels and slash-path the digest check (#9649)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(autofix): pin CI at step level in both verification gates (#9649)

* fix(autofix): drop shadowable gate-body pins, pin gate HOME from staging (#9649)

The two gate bodies' leading statements were bare command words: bash
imports $GITHUB_ENV-planted BASH_FUNC_export%%/BASH_FUNC_unset%% (or
BASH_FUNC_builtin%% for a builtin-prefixed spelling) as functions at
startup even under --norc, and a shadowed pin can arm a DEBUG trap that
swaps the staged runner after the digest check passes and before the
env -i launch executes it — forging the verdict that gates the PAT push.
Both statements are redundant: PATH reaches the child through the env -i
allowlist, and LD_* is closed by the step-level pins, the env execve
prefix, and env -i. Probed: hostile plants fire on the pre-fix body and
are inert on the fixed body; child env is byte-identical without them.

HOME was the remaining $GITHUB_ENV channel into the gate child: npm
resolves its userconfig from HOME, and a planted HOME's .npmrc
script-shell wraps every verdict-determining npm run, so a red branch
reports green (probed: exit 7 becomes exit 0). Capture HOME at stage
time, before any branch code runs, and pin it at step level in both
gates — the trusted_path doctrine. Contract test updated in lockstep:
the pinned statement list drops the two entries, and the pin assertions
cover the HOME pin and its stage-time capture (mutation-probed).

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-22 12:30:42 +00:00
Shaojin Wen
cef54b8340
refactor(autofix): move the push-and-report body out of the workflow file (#9653)
* refactor(autofix): move the push-and-report body out of the workflow file

qwen-autofix.yml was 462,656 bytes — 90% of GitHub's 500 KB start-runs
limit and inside the 470,000-byte gate's warning band. Past that limit
GitHub stops starting runs and says nothing: schedule ticks vanish,
dispatches sit queued with zero jobs, and only PR-event runs keep working
because they resolve an older copy from the PR's branch. The file crossed
it once already, on 2026-08-19, and the loop went dark for a day.

'Push and report' was the largest single block in the file at 626 lines of
inline shell. Its body moves to .github/scripts/autofix-push-and-report.sh
byte-identically — the YAML keeps the step's if: and env:, which is where
the questions of when it runs and what reaches it belong. The file drops
to 422,447 bytes.

Extraction moves the trust problem with it. By the time this step runs the
agent and the verification gate have executed branch code on this host, so
the workspace copy is branch-controlled. The step runs the trusted-base
copy staged before any of that, after proving its digest — recorded in
GITHUB_OUTPUT, which a disk write cannot reach — still matches, that the
staged path is a regular file, and with both reads bounded so a planted
FIFO is a refusal rather than a hang until timeout-minutes. A new contract
test pins all of that, including that the workspace copy is never what
runs; each of its assertions was checked against a mutant of the line it
guards.

The census assertions that counted sites across the workflow now count the
script too, so an extraction cannot silently drop a site from a count that
exists to pin it. The two emit_growth_audit_marker copies are compared
dedented, since one is now at column 0 and its twin is still at ten.

This is also the step docs/design/autofix-gate-runner-isolation.md moves
into its own publish job. Carrying it as a file makes that a small diff,
and deletes the staging scaffolding added here: a job that checks out the
trusted base and never executes branch code can run the script where it
lies.

Refs #9089.

* fix(autofix): close the push-and-report gate's env-import and TOCTOU holes

The wrapper verified the staged script in the step's inherited shell:
a $GITHUB_ENV-planted PATH or BASH_FUNC_* import could swap the gate's
own words, and the staged path was opened three times, so a watcher
could swap the file between the digest check and the execution read.
Run the whole verify-and-run sequence in one env -i clean child that
opens the staged path once, verifies the digest of the captured bytes,
and executes those same bytes. Also fail staging closed on any cp
failure, fix the script's env contract, and pin the new guards.

* fix(autofix): close the push-and-report gate's silent no-op hole

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(autofix): pin gate pass-through, refusal order, and continuation bans (#9653)

Address review suggestions on the push-and-report extraction:

- R2-3: pin the gate child's env -i pass-through list as a sorted
  allow-list equality check (R9-10 precedent), so a dropped variable
  fails loudly instead of expanding empty.
- R4-4: pin the sentinel refusal BEFORE the round_reported write —
  shape-only pins let the ordering rot (probe-verified mutant).
- R4-6: make the execution bans continuation-tolerant; bash joins
  `\<newline>` before tokenizing, so line-continued spellings evaded
  the `[^\n|]*` middle (probe: old regex misses, new catches).
- R1-6: re-scope the paired R10-8 upsert negatives to both halves,
  matching the positive censuses.
- R3-2: correct the stale workflow-size figure (462,656 -> 463,004
  bytes at the post-merge base; 90%/98% still hold).
- R1-5/R1-15/R1-11: correct three disproved claims in the trust-model
  doc (block rank, GITHUB_OUTPUT qualifier, which call sites hold the
  single-open shape).

* refactor(autofix): deliver the push-and-report body as content, not a staged file

The extraction put the body in a file and then had to defend that file: a
staged copy under RUNNER_TEMP, a digest recorded in step output, a type
check, bounded reads, and — after three rounds of findings against those —
an env -i clean child with a liveness sentinel and a 38-entry pass-through
list wrapping the whole verify-and-run sequence. Four review rounds have
now been spent on that machinery, and the open findings against it are
about the machinery, not about the move: output buffering that costs the
live log the inline body had, an env allow-list nothing pins, and pin gaps
in the tests that exist only to hold the gate together.

Delete the object being guarded instead. The stage step already reads the
trusted base before any branch code runs, so it captures the body as text
into step output and the step runs those bytes — the delivery the inline
block had, and the one upsert-deferred-issue.sh already uses. With no
agent-writable copy on disk there is nothing to stage, digest, type-check
or re-open, and no check→use window between the steps: R1-2's TOCTOU,
R1-13's FIFO double-open, R2-2/R2-5/R2-6/R2-7's single-open pins and
R4-1's buffering all describe a mechanism that no longer exists.

This is not a security regression. What it drops is protection the inline
block never had; what it keeps is the one property that matters — the
bytes come from the trusted-base checkout through expression context, not
from a path the branch can write. The step shell's inherited environment
(R1-1, R1-3) is the status quo this PR preserves rather than the thing an
extraction should be fixing: #9525's publish job takes the PAT out of that
shell entirely, which is the real close.

Kept from the gate work: the round_reported output. A loader plant that
kills this shell at execve exits 0 having written nothing, so 'Finalize
autofix status comment' can still tell a published round from a no-op —
now without a sentinel, because the write is simply last.

Also corrected, from the same review: the header no longer claims the
block was the largest in the file (it is third, after 'Scan for PRs with
new feedback' and 'Prepare branch and feedback'), the size is measured at
this branch's merge-base (462,720), the env contract is stated as a rule
rather than a list that went stale within one round, and the shellcheck
note says which lane and which severity it is clean under. The R10-8
execution-ban negatives now cover the scripts as well as the workflow, and
the script's own flag line is pinned as a whole rather than by spelling.

Refs #9089, #9525.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.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>
2026-08-22 10:23:46 +00:00
Shaojin Wen
f89335e453
fix(autofix): stop counting idle timeouts toward the timeout cap (#9673)
* fix(autofix): stop counting idle timeouts toward the timeout cap

TIMEOUT_WINDOW_CAP exists to stop a PR that is too big to finish a round
inside the agent's time budget, and its remedy says exactly that: split
the PR or raise the budget. A silent-sandbox (idle) timeout is a
different failure entirely — the idle watchdog kills the round because
the sandbox produced no output at all, so no budget increase cures it
and nothing about the PR caused it. Counting a failure whose prescribed
remedy is inapplicable is what parked healthy PRs: over the 14 days to
2026-08-21 idle rounds were 58 of 119 timeouts, every one of the 51
windows that tripped this cap tripped it at exactly three, and 9 of the
12 PRs then carrying autofix/needs-human had been stopped here — #8332
at 24 rounds, #8368 at 28, #8276 at 16, all still pushing rounds when
they were parked.

Gate the cap on budget timeouts alone. A persistently wedged sandbox
stays bounded, because an idle round pushes nothing and resets no
streak, so CONSECUTIVE_FAILURE_CAP still terminates it; what no longer
terminates is idle rounds interleaved with real progress, where the PR
is not stuck and the runner is. Idle rounds stay visible through a
job-log warning, which reaches whoever owns the runners without
spending a comment on someone's PR.

Two consequences inside the census. The idle needle became the full
emitted headline prefix rather than a bare substring, because the count
is now subtracted and must be a subset of the total — a loose needle
could match provider error text on the same line and drive the
difference negative. And the all-idle remedy branch is gone as
unreachable: the guard now fires only when budget timeouts alone reach
the cap, so a counted window always holds more of them than idle ones.

* fix(autofix): emit idle census warning on terminal runs (#9673)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(autofix): pin idle-census counts and extend idle exclusion to prepare census (#9673)

* test(autofix): reuse pinned IDLE_HEAD fixture in timeout-census replay (#9673)

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-22 04:48:03 +00:00
Shaojin Wen
83f2589273
fix(autofix): include pending runs in the busy-PR enumeration (#9662)
* fix(autofix): include pending runs in the busy-PR enumeration

The scan's busy enumeration lists live autofix runs server-side by
status, but unions only in_progress and queued. GitHub reports a run
'pending' while its remaining jobs wait on concurrency groups, and the
run-level status trails the job-level flip by minutes — so legs that
were already running stayed invisible to the skip. Measured 2026-08-21
(#9596): one scan re-dispatched four PRs whose legs ran while their run
still listed 'pending'; every duplicate burned one build-cli before
queueing behind the per-PR group it should have skipped, and the
duplicate queued behind #9596's running leg held its dispatch-pending
status open for close to an hour.

Add pending to the union. Pending runs cost one extra jobs-view each
and match nothing until their matrix materialises; the fail-closed
rule and the dispatch-pending marker check are unchanged. The af-026
design record and the test pinning the status union move with it.

* fix(autofix): enumerate busy runs via the runs API, not gh run list --status

* fix(autofix): read the runs-API envelope's id, not the gh-CLI databaseId

The runs-API rewrite of the busy enumeration filtered with
.workflow_runs[].databaseId, but the REST payload has no databaseId
field — that name only exists in the gh CLI's JSON projection. Against
the real API the filter prints one empty line per run, the enumeration
loop skips empty lines, and every scan silently keeps an empty busy
set: busy detection becomes a no-op on every runner, with no
fail-closed trigger ever firing. Verified live: the payload's keys
carry `id`, and `.workflow_runs[].id` returns real run ids for
in_progress, queued, and pending alike.

Fix the filter, pin the field name positively and negatively, and
align the behavioral-replay fixtures with the real REST envelope so
the harness can only pass against the shape the API actually returns.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-21 15:33:47 +00:00
易良
e40263ee55
chore(deps): Clear high-severity CVE baseline and harden the security gate (#9584)
* chore(deps): Clear high-severity CVE baseline and harden the security gate

- Bump OpenTelemetry stack to 0.221.x (fixes @opentelemetry/core advisories)
- Bump @larksuiteoapi/node-sdk to ^1.73.0 and override axios to ^1.19.0
- Bump mobilewright to ^0.0.53 (drops vulnerable sharp 0.34.x)
- Bump markdown-it to ^15.0.0 (drops vulnerable linkify-it 5.x)
- Update undici/fast-uri/brace-expansion/ip-address within range
- Adapt telemetry code to OTel API changes (forceFlush, processor options)
- Make security-checks a hard gate now that the high baseline is clean

* chore(deps): Refresh mobile-mcp vendored lockfile to drop vulnerable sharp

* fix(telemetry): stub sdk-node 0.221 env auto-config helper packages

sdk-node 0.221 extracted its env-based auto-configuration into
@opentelemetry/configuration, otlp-exporter-base, and
otlp-grpc-exporter-base, which it now requires eagerly. The existing
esbuild stub only covered the exporter-* packages, so the OTLP protocol
chain (grpc-js, protobufjs, otlp-transformer) re-entered the sdk-impl
static closure and tripped the serve fast-path bundle guard.

Stub the three helper packages when imported by sdk-node only; our own
protocol modules keep resolving the real packages. qwen-code never
reaches these helpers at runtime (explicit exporters + env scrub).

* fix(telemetry): disable metrics fallback without reader

* fix(vscode): restore nested dependency notices

* fix(deps): declare bundled punycode so its notice survives regeneration

The CLI esbuild config aliases punycode to the userland package
(esbuild.config.js), so the shipped CLI bundle contains MIT-licensed
punycode@2.3.1. Its NOTICES.txt section was lost because the only
lockfile paths reaching punycode were dev-only; the notice walker
(rooted at vscode-ide-companion) never sees a production declaration.

Declare punycode as a direct production dependency of the CLI (the
bundle input) and of vscode-ide-companion (which packages the bundled
CLI into the VSIX and owns NOTICES.txt), then regenerate the lockfile
and notices so the MIT notice is restored.
2026-08-21 07:43:32 +00:00
Shaojin Wen
575e62ee46
fix(autofix): bind the sandbox image to its pulled digest (#9527)
* fix(autofix): bind the sandbox image to its pulled digest

The sandbox image was exported as a mutable tag. `docker run <tag>`
resolves against the local store without re-pulling, so a co-resident
process with daemon access can `docker tag` different content under the
same name between the resolve step and the consumer. Export the
`<repo>@sha256:...` RepoDigests entry that matches both the pulled
repository and the digest the pull itself reported: RepoDigests is shared
by every tag of the same content, so index 0 can move off the pulled repo
under a same-content retag, and retagged foreign content keeps its own
repo — only the pair binds the export to what the pull fetched.

Pin the daemon endpoint for both spawns. The docker CLI resolves its
endpoint from DOCKER_HOST, then --context, then DOCKER_CONTEXT, then
`currentContext` in the pool-shared config.json; clearing DOCKER_CONTEXT
falls through to that last one, so the context is named explicitly and
DOCKER_HOST is dropped from the child environment. An inspect answered by
someone else's daemon hands back any digest it likes.

Write the step files through a non-blocking, type-checked append.
$GITHUB_ENV and $GITHUB_OUTPUT live under the runner-writable temp tree,
where a planted FIFO turns a plain append into a block until the step
timeout.

Extracted from #9214, which is frozen; these were R11-1 and R11-2 there.
The inspect timeout is now injectable so the tests can pin it, and the
suite covers the endpoint pin on both spawns, the FIFO and directory
refusals, cross-chunk stdout accumulation, and the timeout itself. Each
new test was checked against a mutant of the code it pins.

Refs #9089, #9524.

* fix(autofix): bind gate image inputs to the resolver step output (#9527)

* fix(autofix): revert repo-hygiene binding outside PR footprint (#9527)

The deterministic gate rejected the previous commit because
repo-hygiene.yml is CI machinery this PR never touched; review
feedback alone cannot authorize changes there. Restore the file
byte-for-byte and scope the workflow contract test to the two
autofix workflows this PR binds. The repo-hygiene binding is real
and is deferred to the review-findings follow-up queue for a
maintainer-owned change.

* fix(autofix): harden sandbox image consumers per review round (#9527)

- R1-2: extract the duplicated spawn guard (endpoint pin, settle-once
  finish, SIGKILL timer, stdout capture, error/close wiring) into one
  spawnDockerCapture helper; pullImage and repoDigestOf share it.
- R2-1: contract test fails when a workflow detects zero sandbox
  consumers instead of passing vacuously.
- R2-2: success-path e2e test for the digest-bound export; verified it
  kills the exportImage(image) mutant.
- R2-3: pin the daemon endpoint (DOCKER_HOST: '', DOCKER_CONTEXT:
  default) on every sandbox-consuming step, closing the $GITHUB_ENV and
  pool-shared currentContext channels past the resolver; contract test
  enforces the pin.
- R2-4: gate the repair step on the resolver outcome so a failed
  resolver can never relaunch the agent unsandboxed.

Also updates the workflow source pin in scripts/tests to the shared
helper's literals (required by the R1-2 refactor).

* test(autofix): pin repair outcome gate, derive contract set (#9527)

- R3-1: the contract test now requires every always()-gated consumer to
  also gate on the resolver step outcome, pinning the R2-4 fail-closed
  clause; verified that deleting the guard from the repair step now
  fails the suite (the mutant shipped green before).
- R3-2: route both main() e2e tests through withDockerStub; the refusal
  test's untouched-file asserts move before the temp-dir cleanup — they
  previously ran after rmSync, so they passed no matter what the
  resolver wrote.
- R3-3: derive the contract test's protected workflow set from the tree
  instead of a hand-enumerated list, so a new resolver step cannot land
  untested; repo-hygiene.yml stays in an explicit, staleness-checked
  exception set until its deferred binding lands.

* fix(autofix): pin resolver binary, make image check digest-aware (#9527)

* test(autofix): share resolver e2e scaffold, tripwire stale exemptions (#9527)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-21 06:46:52 +00:00
Shaojin Wen
2c64ebe980
feat(autofix): audit the approach instead of stopping on growth-budget breach (#9262)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* feat(autofix): audit the approach instead of stopping on growth-budget breach

A growth-budget breach no longer escalates to a maintainer handoff that
stops the takeover. The breach now makes the round a growth-audit round:
the agent audits the PR's approach on two axes — KISS (name a simpler
alternative or prove each piece load-bearing) and minimal change (every
hunk traces to the problem, an accepted finding, or a failing check) —
and records a machine-readable verdict that the verification gate
requires. sound re-arms the counting window at the current size and the
loop keeps solving; drift simplifies first, then continues; conflict is
the only growth path to a human, parked idempotently until a trusted
human responds.

The old divergence ladder (over budget for N rounds and not shrinking →
stop) terminated takeovers whose remaining work could still fit: the
growth it punished was protocol-mandated pinned tests (#9213 stalled at
round 5 with two small Criticals left). A size signal now triggers a
judgment, never a stop.

Design: docs/design/autofix-growth-audit.md

* fix(autofix): update the artifact-list pin for the growth-audit.json upload entry

* fix(autofix): surface conflict verdicts past the failure.md exits and strip verdict forgery channels (#9262)

* fix(autofix): harden the growth-audit verdict pipeline and park wake set (#9262)

* fix(autofix): close the verdict-pipeline forgeries and loop-generated wake entrances (#9262)

* fix(ci): drop the retired divergence rationale records (af-046/af-047) from qwen-autofix.md

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-21 04:54:07 +00:00
易良
a074d3b042
chore(ci): Disable install scripts in release CI and guard security-checks workflow (#9577)
* chore(ci): Disable install scripts in release CI and guard security-checks workflow

* fix(ci): complete release install hardening

* test(ci): pin release install step count

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): scope release PAT to push step

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): export GH_TOKEN so the release-branch push uses CI_BOT_PAT

* fix(ci): export GH_TOKEN so the credential helper sees it at push time

An inline GH_TOKEN prefix only covers the gh auth setup-git call itself;
the helper re-resolves the token when git push invokes it, so the push
would fall back to the job token with persist-credentials disabled.

* fix(test): anchor setup-git ordering check after the export line

A comment in the push step mentions gh auth setup-git before the export,
so indexOf found the comment first and the ordering assertion inverted.

* style(test): wrap long line to satisfy prettier

* fix(ci): address review findings on PAT handling and install comments

- Pin gh auth setup-git before the git push it authenticates in both
  release and finalize workflow tests, so moving credential setup after
  the push no longer passes.
- Correct the replay comment: npm run generate is not a lifecycle
  script and workspace lifecycle scripts stay disabled.
- Drop the overstated push-boundary claim and record why the push
  needs the bot PAT rather than the job token.

* test(ci): pin CI_BOT_PAT out of install steps and the publish job header

* style(test): apply prettier's exact re-wrap for the two flagged calls

* test(ci): pin CI_BOT_PAT out of the workflow-level headers too

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-21 03:48:03 +00:00
Shaojin Wen
9f2342d323
fix(ci): stop the fallback comment from denying a review it already posted (#9462)
* fix(ci): stop the fallback comment from denying a review it already posted

The review job can fail AFTER posting its review — the CLI exiting
silently, a cleanup step dying — and both fallback sites then announce
that review as one that could not be posted, retry instruction attached.
Measured on PR #9342: the review posted at 11:56:34Z, review-pr failed
at 12:00:53Z ("Qwen review completed but produced no output"), and the
comment landed at 12:01:00Z saying the pipeline "failed before a review
could be posted. … retry with @qwen-code /review" — a fresh ~3-hour
review, asked for beside the review that had just landed. The autofix
takeover loop reads the same feed a human does.

Both sites now check, before composing a body, whether a review this run
posted is already on the PR. The check is scoped three ways so a stale
review can never buy silence on a genuinely dead pipeline: the bot's own
account, the head this run reviewed, and a submission at or after this
run started. Where the proof is unavailable — no start time, no head, a
failed listing — the guard declines to fire and the comment posts, the
same call the head-moved guard already makes.

The job-level step now reads state and headRefOid in one `gh pr view`
(the in-job step already did), which is where its head value comes from.

Tests run the steps' real bash over review fixtures, because the guard
IS a filter: silence when this run posted the review, and posting for
each near-miss on its own — an earlier run's review at the same head,
another account's, one of a different head, a PENDING one, none at all,
an unavailable start time, and a failed reviews listing. One existing
assertion tightened: "no `gh run view`" was the proxy for "no head
comparison on comment runs", and the new guard asks that same command
for startedAt on every event, so it now pins the head lookups
themselves. The stub's state,headRefOid branch learned the pr_closed
scenario its state-only sibling already knew.

* fix(ci): anchor the already-posted guard on the run's creation, and say when it cannot run

Round 1's two blockers, both re-verified against this repo's own run data.

The time anchor reset on job re-runs. `gh run view --json startedAt`
returns the LATEST attempt's start while the run id stays the same — the
dedup above relies on that stability — so a re-run pushed attempt 1's
review outside "this run": runs 32219268680 (created 05:23:57Z,
startedAt 05:51:26Z) and 32218596441 (05:13:04Z → 05:22:05Z) both show
the ~9-28 minute shift. Attempt 1 posts its review, the job fails after
the post, someone re-runs it, attempt 2 fails before posting — and the
guard, anchored on attempt 2's start, lets the contradictory comment
through. Exactly the shape this PR exists to stop, on the path most
likely to reach it. Both sites now anchor on `createdAt`, which is
attempt-stable; a review submitted after the run was created still
cannot belong to an earlier run, so the stale-review protection is
unchanged.

The guard also swallowed its own lookup failures. A transient failure
in either call emptied the value, the guard declined, and the false
comment posted with nothing in the log separating "the guard ran,
nothing matched" from "the lookup died" — while every sibling lookup in
these steps announces its failures. Both unavailable paths now emit a
`:⚠️:` and a step-summary line before posting. No behavior
change: posting was, and remains, the fail-open direction.

Tests: a re-run fixture per site, where the stub answers `createdAt` and
`startedAt` with DIFFERENT values and attempt 1's review sits between
them — reverting either site to `startedAt` fails exactly these two; and
a per-site assertion that both unavailable paths announce themselves.

Also from round 1, both verified before taking: the stub's standalone
`*state*)` branch is dead (no `--json state` call remains in either
extracted step) and is removed, so its scenarios cannot be edited into a
no-op; and the harness now substitutes `${{ vars.* }}` before running
the in-job script, which bash rejected as a bad substitution — the
assignment was skipped, `MAX_TIMEOUT_MINUTES` stayed unset, and eight
error lines rode every suite run, so "the step's real bash" was not
quite true for that line.

* fix(ci): read the head this run reviewed, and claim only what the guard proved

Round 2's six, all taken.

The fallback JOB compared review commit ids against the PR's head at
fallback time, not the head the run reviewed. On every trigger but
pull_request_target the head-moved guard above deliberately does not
run, so a push landing between the post and this step leaves that value
pointing at bytes no review ever covered: the match fails and the
contradictory comment posts anyway — the #9342 shape, re-opened for the
trigger + post + push + fail-after-post interleaving. `review-pr` now
publishes the head its review step recorded as a job output, and the
guard reads it, falling back to the fresh head only when the job died
before that step (a run that posted nothing either). The in-job twin
needs none of this — its unconditional head-moved check exits first —
and that asymmetry is now pinned per site rather than left to be
rediscovered.

Both skip messages claimed "this run already posted a review". Reviews
carry no run id, so the window (bot account + head + submitted at or
after this run was created) also matches an overlapping sibling run's
review, which this workflow's own concurrency note says can happen. The
suppression is right either way — a review IS sitting above the comment
— but the oncall reading the summary was told something the guard never
proved; both now say what it did.

The guard's opening paragraphs still described the round-1 `startedAt`
anchor while the code (and the paragraph below it, and the runtime
warning) said creation. A maintainer reading top-down got the anchor
that re-runs break — the defect round 1 removed.

Test stub: `gh run view` now answers by running the caller's own --jq
over an object carrying both timestamps, instead of a `case` on "$*"
that matched substrings in order. A combined
`--json createdAt,startedAt --jq '.startedAt'` was answered from the
createdAt branch, leaving the re-run pin green for a guard reading the
attempt-scoped field — the exact regression it exists to catch.

* fix(ci): attribute the guard by time alone — the head is not a stable run attribute

Round 3's blocker, and the second time the head clause re-opened the
contradiction this PR exists to close. Two entrances this round, both
after a "Re-run failed jobs": attempt 2 dies before the review step
writes its head, so the guard falls back to a head attempt 1 never
reviewed; or a push lands and attempt 2 records the NEW head — in both,
attempt 1's own review no longer matches `.commit_id`, and the fallback
posts "failed before a review could be posted … retry" beneath the
review the same run had posted.

Rather than patch the head lookup a third time, the head clause is
gone. What the guard proves is now narrower and stable: a bot review of
this PR was submitted while this run was alive — bot account plus the
attempt-stable `createdAt` window. That closes both entrances at once
and takes the round-2 cross-job wiring with it (review-pr's
`expected_head_sha` output and the env line that read it), so there is
no untested chain left whose silent breakage would restore the
fresh-head comparison. The job-level step no longer needs the PR head
either and reverts to its state-only query; the test stub's
state-only branch, removed in round 1 as dead, has a caller again.

The comment blocks now state the guarantee the concurrency model
actually supports. They claimed a review inside the window "cannot
belong to an earlier run", but per-run concurrency groups deliberately
allow overlapping runs on the same head, so an earlier-created run's
review can match and this run's failure then goes unannounced. That is
accepted, and said plainly: the silence coincides with a bot review a
reader can see — the very state that makes the comment's claim false —
while the bot-author and creation-time clauses still rule out silence
with no review at all.

Tests: the moved-head case flips from "posts" to "silences" and is
pinned per site (a review on ANY head inside the window silences);
re-introducing a head clause fails exactly that test; and a structural
pin asserts the wiring is absent rather than merely unused.

* test(ci): skip the guard's jq-driven cases where jq is absent, instead of failing them

The stub answers the guard's reviews and run-view lookups by running the
caller's own `--jq` filter — that filter IS the thing under test — so
those cases need jq on PATH. A reviewer running the suite on Windows
without jq saw them as failures of the guard rather than as untested,
which is the wrong signal in the wrong direction.

Probed once per run and skipped honestly. Measured with a jq that exits
127: the file goes from 31 failures to 26 failures plus 13 skips — the
26 are the retry-loop cases, which have parsed the review log with jq
since long before this change and are equally untestable without it.
GitHub's windows-latest image ships jq, so CI coverage is unchanged
either way; what changes is what a jq-less machine reports.

* docs(ci): remove the head-keyed leftovers the guard no longer has

Round 5's four, all leftovers of the round-3 design change rather than
new behavior.

The job-level block still explained why it compared against the head
this run reviewed — naming `pr_head`, "the reviewed head's review" and a
`review-pr` job output, none of which survive: the shipped filter is
author scope plus the creation-time window, and the wiring was deleted
with the head clause. A maintainer reading it would look for a
comparison that is not there. The in-job block stated the
createdAt-not-startedAt rationale twice, once with the measured run ids
and once without; the measured one stays.

Same in the tests: the stub's comment listed a head clause the filter
deliberately does not have (`attributes by TIME, not by head` is the
test that pins its absence), and the harness still declared and injected
`reviewedHead`/`REVIEWED_HEAD_SHA`, which nothing reads since the wiring
went — a knob that looks live and cannot be.

* docs(ci): drop the duplicated anchor rationale and the last stale-head leftovers

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): exclude foreign same-account approvals from the already-posted guard

* fix(ci): attribute the already-posted guard by composed-review markers

The foreign-approval exclusion list shipped incomplete: the triage skill's
commit-pinned APPROVE body also posts under the same account, matches the
guard's author and window clauses, and silenced the fallback for a
genuinely dead run — the failure shape this guard exists to stop. The
producer set is open, so no exclusion list can be finished; every miss
fails in the dangerous direction.

Match positively instead: a review silences the fallback only if its body
carries what only this pipeline's composed reviews carry — the
"via Qwen Code /review" attribution footer or the invisible
qwen-review-ledger marker. Every composed body carries at least one (a
zero-findings APPROVE included); no foreign approval carries either. A
marker that ever changes shape stops the guard firing and the comment
posts — the pre-guard status quo, not a masked dead run.

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-20 23:51:05 +00:00
Shaojin Wen
c59910ba3f
fix(ci): make autofix finding replies idempotent (#9463)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* fix(ci): make autofix finding replies idempotent

A crash-and-rerun of an address round, a same-run repair that regenerates the dispositions, or a later round re-declining the same finding all reproduce the same comment-replies.json entry — and the reply step posted it again, landing identical bot replies on one thread (observed 2026-08-16: one identical reply posted three times, #9296).

The thread fetch now also reads each comment's author and body, and the reply step skips posting when the thread already carries a comment by the autofix bot whose body equals the neutralised body about to be posted. A changed body — new information from a later round — still posts; a threads view without author/body, or a stale/empty one, degrades to the old post-always behavior. The replies API itself is already the no-review-event path, so this PR only adds the missing idempotence (the P1 replies item of #9296).

Refs #9296

* fix(ci): restore inner pageInfo in autofix threads query and pin reply-gate contracts (#9463)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-20 14:10:26 +00:00
Shaojin Wen
4807d06b31
fix(autofix): mutation-probe new guards before a round commits (#9578)
* fix(autofix): mutation-probe new guards before a round commits

Provenance analysis of six multi-round takeover PRs found roughly a
third of post-initial review findings were introduced by the
immediately preceding fix round, and the dominant shape was guards
and branches added with no test of their own: the deterministic gate
re-runs only the tests that exist, so an unwitnessed guard passes
every gate and its hole resurfaces as a new finding in a later
round. Require the agent to prove each newly added guard or branch
kills a test (mutation probe) before committing, and record the
probe in the round summary.

* fix(autofix): pin the mutation-probe mandate, not just its intro

The original pin stopped at the preposition "before", so a future
edit that kept the intro phrase but gutted the remove/confirm-FAIL/
restore procedure would leave the contract test green. Pin both
halves of the mandate. Mutation-verified: gutting the mandate in
SKILL.md now fails the skill contract test.

* fix(autofix): also pin the mutation-probe remediation clause

The procedure pins cover what the probe does, but not what to do
when it exposes an unwitnessed guard: dropping the remediation
sentence ("write a test that pins it (or drop the guard)") left the
whole suite green while the rule told the agent to probe without a
verdict path. Pin the remediation clause too; mutation-verified.
2026-08-20 14:03:00 +00:00
Shaojin Wen
099a71c936
fix(ci): heal a symlinked workspace instead of wedging the runner on it (#9498)
* fix(ci): heal a symlinked workspace instead of wedging the runner on it

The hardened wipe guard refuses any workspace that canonicalizes outside
the runner workspace. That refusal is correct, and it created a permanent
failure: when a previous job leaves the workspace replaced by a symlink
pointing outside — or by any non-directory — the guard resolves it to the
target, refuses, and exits 1 having removed nothing. Nothing else clears
that state, so every later job on the runner dies at the same line,
forever. The pre-guard code wiped through the link and self-healed by
accident. Reproduced against main's own step text before this change.

Heal it: the link itself lives inside the runner workspace and is safe to
unlink, and only once it is gone can a legitimate wipe proceed.

The layer has to sit before canonicalization — afterwards the path has
already resolved to the target and the allowlist refuses before any repair
can happen — which means it judges a raw path, and that is where the first
attempt at this (closed with #9369) went wrong. A raw `"$RWS"/*` match
accepts `$RWS/link/sub` as a string while the kernel resolves it through
an intermediate symlink to a file outside the runner workspace, so the
unlink and the mkdir landed outside and only then did the allowlist refuse
the wipe. Here the containment is judged on the canonicalized PARENT —
never on $WS, which would resolve through the very link being removed —
and the unlink then acts on the raw path, so it takes the link and never
follows it.

Four more constraints the same review surfaced: the raw trailing-slash
strip moves ahead of the predicates (both `[ -L "$WS/" ]` and
`[ ! -d "$WS/" ]` resolve through a link and report its target, so one
slash hides the corruption); the allowlist root is prepared before the
heal, since it bounds it, and an empty $RUNNER_WORKSPACE would degenerate
the containment pattern to the match-all `/*`; both the unlink and the
mkdir fail closed, because under `-e` a failure that is not the last
command of an && list is swallowed and would leave the wipe running on a
corrupt path; and the heal logs what it found and where the link pointed,
since this incident otherwise leaves no trace at all.

All three copies get it — the two triage wipes and the A/B wipe — with
per-suite fixtures: the wedge healed (link gone, directory recreated,
target's contents intact), the intermediate-symlink attack refused with
the outside file unmutated and zero rm calls, the non-directory half, the
trailing-slash spelling, the fail-closed unlink, and the ordinary
workspace where the heal must not fire at all. Mutation-checked layer by
layer; each has a fixture that fails when it is removed.

One pre-existing test changes meaning: the canonicalization pin used a
symlinked workspace and asserted refusal, which is now the healed path. It
moves to a vector the heal does not touch — an intermediate symlink whose
far end is a directory — and keeps its mutation strength: with the
canonicalization deleted, find resolves the link and hands the outside
directory's entries to the rm recorder.

Closes #9480

* fix(ci): keep the heal's log out of the workflow-command channel

Three findings from the first review round on this layer.

The heal logged the symlink's target inside a `:⚠️:` line. The
target is bytes a PREVIOUS job chose — on the verify lane that job may
have run a contributor's code — and the runner parses `::` at the start
of any stdout line as a workflow command, so a target of
$'…\n::error::forged' let the step reporting the corruption forge an
annotation. The annotation now carries no untrusted bytes: the target is
stripped of line breaks, capped, and printed on its own prefixed line,
where a leading `::` cannot begin a command. Verified against the real
step text — the forged line lands as data, and no output line starts with
`::error::`.

The mkdir leg's refusal had no executed fixture while its `rm -f` sibling
had one. It does not need a permission trick: `rm -f` returns 0 for a
path whose parent is not a directory (it reads as "already absent"), and
the mkdir that follows cannot succeed — so the branch is reachable, and a
swallowed failure there would run the wipe against a path that does not
exist. Fixtures in both suites, and it runs as root too.

And the post-run triage copy's header still said this copy "predates the
checkout-heal hardening and never received it" while carrying the whole
guard plus the heal directly underneath. That header is the in-code
inventory the eventual convergence of these copies will read; understating
it is how a sync strips layers in the wrong direction.

* test(ci): drive both wipe copies in the remaining single-step heal fixtures

* fix(ci): keep the Serve A/B job from timing out on slow runners

---------

Co-authored-by: Qwen Autofix <autofix@qwen-code.dev>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-20 13:54:36 +00:00
Shaojin Wen
6fe9ce4886
fix(ci): stop counting wedged queued runs as in-flight in the shepherd (#9518)
* fix(ci): stop counting wedged queued runs as in-flight in the shepherd

When GitHub refuses to start a workflow run it still CREATES it: the run sits
`queued` forever with zero jobs and cannot be cancelled or deleted through the
API. On 2026-08-19 an oversized qwen-autofix.yml produced a run like that from
the shepherd's own liveness dispatch, and the watchdog counted it as in-flight
for the next 18 hours:

  last scan signal: 2026-08-19T05:01:14Z (1107m ago), in-flight: 1

The age gate said "dispatch a scan", the in-flight gate said "one is already
running", and nothing ever completed the run that would clear it. The loop
stayed dark until a human looked.

Treat a run still `queued` past ZOMBIE_QUEUED_MINUTES (30, overridable via the
QWEN_SHEPHERD_ZOMBIE_QUEUED_MINUTES repository variable) as wedged rather than
live. One `wedged` predicate is defined once and reused by the in-flight count,
the conflict lever's busy-set, and a new census, so the three readers cannot
disagree. Only `queued` runs wedge — a review-address run legitimately runs for
hours — and a missing createdAt reads as brand new, so unknown age never
licenses a duplicate dispatch.

The wedge is now visible instead of silent: a :⚠️: names the count and
the oldest one, the tick heartbeat carries `wedged-queued:`, and the dashboard
carries a banner. Invisibility is what made this expensive — PR-event runs kept
reporting success while every scheduled scan was dead.

Verified against the real run list from the incident: the old predicate returns
in-flight=1 (starved), the new one returns 0 with a census of 2. Behavioral
tests replay both jq programs and the busy-set walk verbatim from the workflow.

* fix(ci): keep shepherd busy-set job-verified and bound wedge re-dispatch (#9518)

* fix(ci): reject degenerate zombie threshold and name the paused liveness gate (#9518)

* fix(ci): name the recorded liveness run in the shepherd wedge remedy (#9518)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-20 13:37:22 +00:00
Shaojin Wen
313f191150
fix(autofix): make the brake's BLOCKED handoff a first-class round outcome (#9297)
* fix(autofix): make the brake's BLOCKED handoff a first-class round outcome

When the growth brake fires, feedback.md tells the address agent to stop BLOCKED with a handoff — but the output contract only accepted address-summary.md or no-action.md, so a round that followed the instruction died as 'finished without required output file(s)', the brake's decision text was buried under a generic failure.md, the report said 'could not produce a passing fix', and the job left a red review-address check that the next scan counts as new feedback. Observed on #9222 rounds 6/7.

The handoff becomes a first-class verdict end to end: run-agent.mjs honors an agent-written handoff.md (with no fix verdict) as a graceful exit the way it already honors failure.md, and shields it from the API-error retry reclassification; the verification gate reports outcome=handoff for a no-commit round with a handoff and no failure.md; finalize lets handoff pass without failing the job; the report step runs for this outcome, posts the handoff note with the eval marker (watermark advances — the feedback is consumed as evaluated), and names the stop honestly instead of reporting it as a failed fix. The skill now tells the agent exactly which file to write when the brake fires. A coexisting spec output still outranks the handoff, and failure.md coexistence keeps the failed classification, so crash paths are unchanged.

* fix(autofix): align the handoff outcome's consumers and pins with its contract (#9297)

Review found the new handoff outcome breaking two pinned helper tests
(stale breaker-headline wording, unclassified headline in the fleet-shepherd
contract test), misreporting handoff rounds in the status-comment finalize
step, and leaving the whole handoff chain unpinned against mutation.

- Update the breaker headline pin to the PR's reworded headline.
- Classify the handoff headline as transient in the shepherd contract test
  and drop its "AutoFix stopped" prefix so the shepherd's terminal-only
  REASON regex cannot capture a transient stop (the shepherd workflow itself
  stays outside this round's footprint).
- Include handoff in the Finalize-status published-report branch.
- Give deliberate stops their own takeover-digest census bucket instead of
  the residual crash/infra bucket (EN + ZH).
- Neutralize :: workflow commands at the two new handoff echo sites.
- Use the runner's non-empty missing() convention for handoff.md so an
  empty file cannot read as a verdict in one layer and not the other.
- Correct the run-agent.mjs precedence comment: when a handoff coexists
  with a spec output, the gate (handoff branch first) decides the round,
  matching the documented "handoff + no-action -> handoff" contract.
- Pin the handoff chain where its siblings are pinned: finalize replay,
  POST_HANDOFF replay, mark/headline replays, the gate's no-commit decision
  table, the stub-runner handoff/empty/API-error cases, the report-step
  if-clause, and the census needle-to-emit cross-pins.

* fix(autofix): classify a no-commit handoff before the gate's structural checks (#9297)

Review proved the new handoff classification unreachable exactly where
the brake fires: the structural pre-checks (core rebuild, settings
schema, contracts) judge the PR's own diff and reject before the
no-commit fork, and the growth brake fires on precisely the red PRs
whose diff trips them. A compliant handoff (no commit, only handoff.md)
then classified as a retryable failure, so the repair pass deleted
handoff.md and could commit against the brake's explicit stop — the
self-feeding loop the handoff exists to prevent. Reproduced with the
real gate script: schema-check-fail + no-commit handoff exited 1 with
no outcome=handoff.

Move the no-commit handoff classification above the structural checks
(right after the failure.md exits, which keep their precedence). A
handoff claims nothing — acted=false, deferred to a human — so the
checks' false-no-action rationale does not apply, and the retryable/
repair machinery must never engage on a round the brake told to stop.
The no-op fork reverts to no-action-only classification.

- Add a gate test: stale schema + no commit + handoff.md classifies
  outcome=handoff, exit 0, no retryable (fails on the pre-fix gate).
- Pin the handoff-note :: workflow-command neutralization in both
  layers (the gate's sed and the runner's replaceAll), which review
  showed were surviving mutations.

* fix(autofix): reject a no-commit handoff written over a dirty workspace (#9297)

* fix(autofix): report a dirty-handoff rejection honestly, not as a failed fix (#9297)

* fix(autofix): reject a handoff written beside a round commit, non-retryably (#9297)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): classify the committed handoff shape as its own non-retryable outcome

R7-1 on this PR: a round that HAS a commit beside handoff.md skipped both brake-violation guards (clean tree misses the dirty guard; committed ref misses the no-commit branch) and fell through to the structural checks, where reject_fix defaults to retryable and the repair pass deletes handoff.md and may commit again against the brake's stop.

Classify it before the structural checks under its own outcome committed_handoff, sibling of dirty_handoff: non-retryable, its own honest report headline (reusing dirty_handoff's wording would claim nothing was committed when a commit exists), listed among the report-publishing outcomes in the status classifier, and never routed through finalize's pass list. Pins updated in the same pass: the committedWithHandoff gate case now expects committed_handoff with no retryable, the shepherd contract test classifies the new headline as transient (loop stays engaged), the status-classifier pin names all five outcomes, and the handoff-contract gate test gets an explicit subprocess budget (eight fixture arms outgrew the 5s default).

* fix(ci): count committed-handoff rounds in the milestone census rejected bucket (#9297)

* fix(autofix): publish brake violations green and preserve handoffs across crashes (#9297)

Two Critical review findings on the handoff output contract.

Brake-violation rounds (dirty_handoff / committed_handoff) ended with a
red review-address check: the eval marker stamps ts=NEWEST, strictly
before the check completes, and the scan counts failed checks completed
after the watermark — including this workflow's own review-address
checks — as new feedback. The next scan re-selected the PR and burned a
full agent round on the item the posted headline promised not to retry,
once per violation. Admit both outcomes to the green finalize arm the
way the clean handoff already is (the diff's own comment names this
self-feeding loop as the reason handoff went green), and key the report
step's routing and POST_HANDOFF trigger on the outcomes themselves so
the green rounds still publish their honest headline, handoff note, and
eval marker instead of going silent.

A crash, budget kill, or loop guard after the agent wrote handoff.md
synthesized a failure.md that shadowed the note: the gate reads
failure.md first (outcome=failed), the report preferred it, and the
timeout sentinel re-handed the item the brake stopped. Preserve the
agent-written handoff in the crash branch (exit 0, mirroring the
agent-written-failure.md arm), and never let writeHandoff overwrite a
non-empty agent verdict.

Both findings reproduced against this commit's verbatim code before
fixing: the case/jq replay showed the violation check red and counted
as new feedback, and a stub run showed the synthesized failure.md
shadowing the handoff. New behavioral tests fail pre-fix and pass
post-fix.

* test(ci): give four subprocess-heavy replays explicit budgets

The milestone digest, stale-duplicate revalidation, deny-by-default footprint, and recoverable-API-render tests spawn multiple bash replays of the real workflow/gate scripts each; the files those replays parse grew with this PR's handoff chain, and all four outgrew the 5s default (each verified to pass with an explicit 30s budget, matching the suite's convention for subprocess-heavy tests).

* fix(ci): mirror the handoff outcome consumers into the recovery clone (#9297)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-20 08:32:43 +00:00
易良
0baaec2b32
chore(ci): Drop NPM_TOKEN in favor of npm Trusted Publishing (#9552)
* chore(ci): Drop NPM_TOKEN in favor of npm Trusted Publishing

* chore(ci): Pin npm 11 for Trusted Publishing in release jobs

* test(ci): Cover Trusted Publishing requirements
2026-08-20 08:31:41 +00:00
易良
48b30647d0
refactor: centralize cross-package contracts (#9497)
* refactor: centralize cross-package contracts

* fix(build): harden cross-package contract checks

* docs(core): clarify sub-session prompt limit scope
2026-08-20 06:24:41 +00:00
易良
b219e3a716
chore(ci): Add --provenance to npm publish and id-token permission (#9532)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* chore(ci): Add --provenance to npm publish and id-token permission

* test(scripts): expect --provenance in npm publish step assertion

PR #9532 adds --provenance to every npm publish in the release pipeline.
Update the workflow-pinning test to match the new command so the helper
test suite stays green.
2026-08-20 05:21:02 +00:00
Shaojin Wen
3b3818db87
fix(ci): keep qwen-autofix.yml under GitHub's 500 KB start-runs limit (#9517)
GitHub does not start runs for a workflow file larger than 500 KB (512,000
bytes) and reports nothing when it stops. qwen-autofix.yml crossed that line
on 2026-08-19 at 512,782 bytes: schedule ticks stopped firing, every
workflow_dispatch sat "queued" forever with zero jobs and could not be
cancelled, and issues/issue_comment went quiet — while pull_request_review
runs kept succeeding, because a PR event resolves the workflow from the PR's
own branch and those carry older, smaller copies of this file. The loop
therefore looked half-alive and stayed dark for a day.

Move 75 long comment blocks (1,326 lines) verbatim into a sibling design
record, .github/workflows/qwen-autofix.md, leaving each block's opening lines
plus a `qwen-autofix.md#af-NNN` pointer where it sat: 518,055 -> 426,437
bytes. No executable line changes — the YAML parses to an identical document
outside `run:`, every `run:` script still passes `bash -n`, and the only lines
removed anywhere are comments. Steps that are duplicated verbatim across jobs
share one pointer so they stay byte-identical.

Add .github/scripts/check-workflow-size.sh (gate at 470,000 bytes), wired into
CI on every profile: a .github-only PR classifies as `github_ci_only` and
skips the `full`-only checks, which is exactly the PR that can trip this.
Tests pin the gate, every workflow's size, and pointer/section symmetry.

Delete qwen-autofix-recovery.yml. It was cloned during the incident on the
theory that the workflow ENTITY was wedged, but it carried the same oversized
file, so its dispatches queued identically and its schedule never fired.
2026-08-20 01:56:45 +00:00
Shaojin Wen
133cf8bfcf
refactor(cli): consolidate shared helpers ahead of the legacy audit skill (#9345)
* refactor(cli): consolidate shared helpers ahead of the legacy audit skill

Move the pieces the upcoming /audit skill needs out of command-group
ownership so no skill imports across command groups:

- the findings schema moves from commands/review/ to cli/src/utils/ as-is;
  every review consumer imports it from the new home, and the stale-bundle
  digest, bundle-asset list, and artifact comment track the move
- safeTarget (traversal-safe slug) and tokenizeArgs (quoted argument
  splitting) lift to cli/src/utils/paths.ts and shell-args.ts, with
  review's copies re-exporting/redirecting
- the two private git check-ignore copies (review test-plan, team memory)
  consolidate into one fresh-by-default helper in core utils; the memo
  stays caller-side so a remedy re-check observes the flip

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): restore review ledger machinery dropped by the consolidation

The shared-helper consolidation silently reverted three behaviors PR #9175
added to compose-review, against the PR's stated "no behavior change"
intent: the unreviewed-dimension anchor exemption (scopeUnproven /
dimensionGapsAreDepthOnly / isNonDiffDimensionGap), the LEDGER_MAX_ROUND
stamp clamp, and the bilingual budget-stop phrase splice. Restore them with
the tests that pin them; SKILL.md, ledger.ts, and deadline.ts still
document all three.

Also harden the new helper tests:
- safeTarget: the deep-path fixtures now share a flattened prefix longer
  than the kept window, so a truncation-only slug (no digest) collides
  instead of shipping green.
- isGitIgnored: each GIT_* scrub arm now carries a discriminating fixture
  (three arms previously passed with their scrub line deleted), and every
  foreign git init scrubs ambient repo-placement selectors.

* fix(core): scrub git config-injection channels from the ignore probe

Ambient GIT_CONFIG_COUNT (inline KEY/VALUE injection) and
GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEM (config-file redirects) can aim
core.excludesFile at a foreign rules file and flip the probe's verdict
for the -C worktree — the same leak class the existing selector scrubs
close. Measured on the pristine probe: both channels turn a
not-ignored path into an ignored one.

Also pin the guards the review found unpinned: the two config
channels, the `--` separator for dash-leading paths, the timeoutMs
wiring, safeTarget's hash-of-original-target property, and
tree-existence of the lifted review helpers.

* fix(core): scrub pathspec-magic env channels from the ignore probe

* fix(core): drop the whole GIT_* env family from the ignore probe

Two more leak channels surfaced on the probe's per-variable scrub list:
GIT_ICASE_PATHSPECS (the fourth pathspec modifier — ambient, makes
check-ignore reject every pathspec with exit 128, which the catch reads
as not-ignored) and GIT_CONFIG_PARAMETERS (the inline -c channel git
itself uses to propagate config to children — ambient, can aim
core.excludesFile at a foreign rules file). Measured through the real
function: the first flips a genuinely ignored path to not-ignored, the
second flips a not-ignored path to ignored.

Since the channel list grew by one leak per review round, drop the
whole GIT_* family instead of enumerating, and close the system config
tier explicitly (GIT_CONFIG_NOSYSTEM=1) so host policy in
/etc/gitconfig can no longer answer for the -C worktree — that ambient
dependency also made the config-redirect arm red on any host whose
system config matches the probe path. Pin the icase member, the
PARAMETERS channel, the default 5 s deadline (previously unpinned), and
add a lower timing bound to the caller-deadline arm so it cannot pass
vacuously when the shim is not executable.

* fix(cli): make the safeTarget slug space prefix-free at the dash boundary

Review's cleanup sweeps .qwen/tmp/ by qwen-review-<slug>- prefix. A slug
that itself carried '-' — natively (pr-6771 vs pr) or via the truncation
join — could extend a shorter slug, letting one target's cleanup delete a
DISTINCT target's artifacts (R8-7). The truncation branch this PR carries
newly lands deep targets inside the cap instead of dying ENAMETOOLONG,
which turned the latent collision live.

Drop '-' from the slug alphabet entirely (dashes flatten like separators)
and join the truncation digest with '_': with '-' out of every slug, the
qwen-review-<slug>- boundary is unambiguous by construction — no slug can
start with another slug plus '-'. prev-ledger side files keep their
hardcoded dashed name on both writer and reader, untouched by the slug.

Tests pin the prefix-free property (short-vs-short, short-vs-truncated)
and the fixture names follow the new slugs.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* revert(cli): restore safeTarget byte-identical to the pre-lift behavior

Reverts the slug alphabet change (539495226d) and drops the deep-target
truncation branch, per review round findings R12-1 and R8-7:

- R12-1: flattening dashes renamed only one side of review's two-sided
  artifact-naming contract — bundled-skill templates, composed names,
  prev-ledger and brief/report producers hardcode the dash spelling, so
  the bypass-audit tripwire would silently skip and the cleanup sweep
  would leak ~15 dash-form artifacts per review.
- R8-7: the prefix-sweep hazard only exists because of the truncation
  branch this lift carried; main's safeTarget has no truncation, so a
  behavior-preserving lift must not add it. Deep-target support belongs
  in a follow-up paired with the sweep-side structural fix it needs.

safeTarget is now byte-identical to the pre-lift implementation (moved,
not modified); tests pin the dash spelling and the leading-strip rule as
they behave on main. Full review suite green (5022).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-19 14:53:44 +00:00
易良
b5577b7d11
fix(sdk): route unrecognized diagnostics onto a bounded transcript sidechannel (#9202)
* fix(sdk): route unrecognized diagnostics onto a bounded transcript sidechannel

Normalizer-classified unrecognized_event / unrecognized_session_update debug events no longer enter transcript blocks[]: they are mirrored onto a capped unrecognizedDiagnostics sidechannel instead. This stops them from finalizing a streaming assistant/thought block (which dropped a following assistant.usage frame) and from consuming the maxBlocks budget (which let repeated noise evict real conversation content). malformed_payload diagnostics and client-dispatched debug events keep their existing block semantics.

* fix(sdk): align browser bundle budget

* fix(sdk): close the sidechannel review round (#8823)

- export the sidechannel API through the daemon barrel
  (selectUnrecognizedDiagnostics, UNRECOGNIZED_DIAGNOSTICS_LIMIT,
  DAEMON_UI_UNRECOGNIZED_DIAGNOSTIC_REASONS + types) and pin the
  reachability in daemon-public-surface.test.ts
- restore the MAX_TEXT_BLOCK_LENGTH cap on sidechannel text, mirroring
  truncateText exactly (suffix fits within the cap)
- ship the unrecognized reason subset as a runtime const array and route
  by membership, so a new reason cannot fall through to appendStatusBlock
- copy the correlation fields createBase stamps (promptId, sourceRecordIds,
  branchRecordId, originatorClientId) onto sidechannel entries; drop the
  dead source/data switches
- un-fuse the budget-history comment chain in scripts/build.js
- update docs/developers/daemon-ui for the split routing
- tests: full entry shape, text cap, block-path debugReason counterpart,
  and a webui malformed_payload interleave sibling so the #7012
  flush-before-guard keeps a discriminating stimulus

* fix(sdk): address round-2 sidechannel review for #8823

- build.js: bump daemon browser bundle budget 191KB -> 192KB
  (195,591 bytes measured > 195,584 cap; build failed at head)
- webui: narrow the observer-mode debug guard so unrecognized_*
  diagnostics reach the reducer sidechannel; only block-path debug
  events are dropped
- webui: merge history-store unrecognizedDiagnostics in
  applyTranscriptHistory so paged-back sessions keep diagnostics
- transcript: extract truncateTextAtLimit shared by the block and
  sidechannel truncation paths
- transcript: reset unrecognizedDiagnostics on rewind alongside the
  sibling per-turn state resets
- types: rename DaemonUnrecognizedDiagnostic.receivedAt to
  clientReceivedAt (matches the sibling block projection)
- tests: reason-prefix conformance pin, rewind reset, narrowed guard,
  history pagination merge

* fix(webui): avoid flushing sidechannel diagnostics

* fix(sdk): preserve diagnostics across rewind

* fix(webui): dedupe sidechannel history records

* fix(webui): align the paging sidechannel test with the normalizer keys

The paging test added in e6b40e5c failed deterministically (webui
suite red, CI Test job red) for two reasons:

1. The fixtures stamped only _meta['qwen.session.recordId'], but the
   SDK normalizer's extractSourceRecordIds reads
   _meta.qwenTranscript.sourceRecordIds — no sidechannel entry ever
   carried sourceRecordIds, so the dedupe assertion could not pass and
   the new displayedRecordIds loop was never exercised by a passing
   test. Stamp BOTH keys, matching production replay frames
   (acp-bridge buildUpdateMeta) and the sibling dedupe test.
2. Cap arithmetic: LIMIT-1 live entries + 2 fresh history entries =
   LIMIT+1, so the newest-wins slice evicted record-old-1 which the
   test asserted present. Emit LIMIT-2 live events so the post-merge
   total lands exactly on the cap.

Also correct the post-merge index assertions: history entries come
first (old-1, old-2), then the deduped-once live overlap, then the
first live mystery event. Suite 506/506, eslint + prettier clean.

* fix(sdk): raise diagnostic sidechannel bundle budget

* fix(sdk): raise the daemon browser bundle budget to 198KB and pin the diagnostics selector

- The sidechannel routing + selector cost ~1037 B over the 197KB cap
  (bundle measured 201893 B), failing the browser-bundle size gate; bump
  MAX_DAEMON_BROWSER_BUNDLE_BYTES to 198 * 1024.
- Fold the rebase-residue 190→191→192 KB ledger entries into the accurate
  190→195→196→197→198 lineage so the next bump has one canonical history.
- Add a behavioral pin for selectUnrecognizedDiagnostics: it must return
  the routed sidechannel itself (toBe), discriminating a `return []` or
  shallow-copy regression that the typeof-only surface test cannot see;
  flip-verified.

* fix(sdk): reset the user pointer on sidechanneled diagnostics, share the routing predicate

appendUnrecognizedDiagnostic left activeUserBlockId untouched while the
replaced appendStatusBlock path reset it for every non-user block; a
later mergeable user.text.delta with no promptId stamp (e.g. a peer
client's $ <cmd> echo) then appended onto the earlier user block
across the diagnostic, collapsing two user turns into one and skewing
rewindTranscriptToUserTurn's kind==='user' turn indexing. Keep the
reset (assistant/thought pointers stay untouched, the point of the
sidechannel); witness test flip-verified red without the one-line reset.

Also export isUnrecognizedDiagnosticReason from types.ts next to
DAEMON_UI_UNRECOGNIZED_DIAGNOSTIC_REASONS and call it at all three
routing-guard sites (reducer, provider flush condition, provider drop
filter) so the #7012/#8823 guard pair classifies every debug event
against one source instead of three hand-written copies.

* fix(ci): prevent bite harness SIGPIPE

---------

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-08-19 14:38:00 +00:00
Heyang Wang
5003ab3c7f
feat(web-shell): add transcript contract prevalidation (#9388)
* test(web-shell): add transcript contract prevalidation

Freeze reproducible evidence for current transcript paths before any
VS Code or HTML export production migration.

- Add versioned fixtures, closed export schema, and capability gates
- Probe direct-daemon and ACP identity under partial history prepend
- Preserve raw adapter semantics and full write_file Turn Output diffs
- Document the two-MR architecture, security constraints, and blockers

* fix(web-shell): harden transcript prevalidation gates

Make the evidence-only contract suite enforce the review assumptions it
documents while preserving the existing runtime transcript behavior.

- Run the contract suite in the required no-AK integration job
- Fail closed on ambiguous identity probes and deduplicate gate kinds
- Enforce manifest, hash, export safety, and renderer version boundaries
- Cover visible transcript text and stable Desktop packaging semantics
- Record the complete PR comment evaluation and verification outcome

* fix(web-shell): close transcript prevalidation gaps

* fix(web-shell): remove brittle Desktop wiring probe

Keep transcript contract prevalidation at the evidence level it can
actually prove. The previous source-text assertion could both reject
equivalent formatting and pass unreachable packaging code.

- Remove the Desktop script parser and its false behavioral claim
- Mark installed-artifact verification as deferred to Desktop smoke tests
- Clarify MR1 matrix, CI wiring, and provenance evidence boundaries
- Refresh the hash-locked capability matrix fixture

Note: This does not change Web Shell or Desktop production behavior.

---------

Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
2026-08-19 14:13:12 +00:00
Shaojin Wen
1eb8a0c7f8
feat(review): wire --resume through /review and the review run subcommand (#9153)
Surface the local resume feature (PR #9092) on the paths a user reaches
it from:

- `parse-args.ts`: `/review <pr> --resume` parses to
  `resume: { requested, effective }`, gated on PR targets (a local
  review's diff comes from a live working tree with no stable interrupted
  state). A `--resume` on a non-PR target warns and is inert.
- `run.ts`: the `qwen review run` headless wrapper takes `--resume` and
  passes it through to the `/review` prompt.
- `SKILL.md` Step 1 gains a "Resuming an interrupted run" branch: on
  `resume.effective`, append `--resume` to `fetch-pr`, branch on its
  `resumed` JSON, run `recover-findings`, re-enter the audit loop at
  `latestReverseAuditRound + 1`, and read the restart bound back from
  `restartsSpent`.
- `DESIGN.md` / `docs`: document resume as a LOCAL convenience.

The CI review workflow runs FRESH — it does not pass `--resume`. A CI
attempt runs no-sandbox on the reviewed PR's own code and its worktree is
deleted the moment it exits, so there is no interrupted state on disk for
a retry to continue; a resume would refuse `worktree-gone` and start over
anyway. The retry loop and its test assert the fresh-only wiring.
2026-08-19 05:13:09 +00:00
qqqys
b6e93d27ad
fix(autofix): paginate review threads instead of reaching the oldest 100 (#9390)
* fix(autofix): paginate review threads instead of reaching the oldest 100

`resolve_and_reply_threads` fetched `reviewThreads(first:100)` with no
pagination. GitHub returns review threads in ASCENDING creation order, so a
single page is the OLDEST hundred — on a long-running PR, precisely not the
threads the current round is answering.

Both blocks downstream map an inline-comment id to its thread. A thread past
the page is absent from `THREADS_JSON`, so an implemented Critical is never
resolved and reads as still open, and a declined finding's reply is answered
by silence. Those are the two outcomes the function exists to prevent.

Live: 8 of the 22 open takeover PRs exceed the cap. #8403 carries 1256
threads, so one page reached 8% of them — and all 1256 are unresolved.

The code already detected this: it requested `pageInfo{hasNextPage}` and
emitted a `:⚠️:` when true. It just never fetched the next page.

Use `gh api graphql --paginate`, which is built for exactly this shape, and
slurp its node stream into the flat array both blocks already expect. On
#8403 that is 13 requests in ~10s.

A partial fetch is USED rather than discarded: losing twelve good pages to a
rate limit on the thirteenth would resolve nothing at all, so the failure is
announced and the threads in hand still map.

One residual stays open and is now announced rather than implied: a thread
carrying more than 100 comments still truncates, so a comment past that page
is unmapped and each block falls back to the id as given. No thread in the
live pool comes close.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(autofix): keep the pagination failure's reason, and pin both warnings' absence

Round 1's three Suggestions, all on the partial-fetch path this PR adds.

R1-1: `2> /dev/null` on the paginated fetch discarded gh's stderr — the only
text saying WHY pagination stopped. The warning announced THAT it stopped, so
the oncall could not separate a transient rate limit (back off) from an
expired PAT (rotate) or a network failure without re-running the ~13-request
query by hand. Captured to `${WORKDIR}/threads-fetch.err` with the pattern
already used elsewhere in this workflow, and its tail folded into the warning.

R1-2: the outer thread pagination silently depends on the inner `comments`
pageInfo NOT asking for `endCursor` — gh's paginator adopts the first pageInfo
carrying both fields. The `Residual:` note actively invited a maintainer to
close that residual by adding it, which would hijack the thread-page cursor
and stop after page one at exit 0 with no warning, silently restoring the
oldest-hundred bug. Documented as load-bearing, in the comment block above the
fetch rather than inside the query literal — a `#` line there is transmitted.

R1-3: both new warnings were asserted only in the positive, so a mutation
making either unconditional shipped green. Added the clean-run absence
assertions this file's own convention calls for (321 `not.toContain` uses),
and the gh stub now writes a reason to stderr on failure so the folded-in text
is assertable.

Verified: qwen-autofix-workflow 178 passed. Mutation-checked — restoring
`2> /dev/null` and making the pagination warning unconditional each fail a
test. The one remaining failure (`behaviorally replays the stale-duplicate
revalidation`, 5s timeout) is identical with these changes stashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(autofix): keep gh's error body out of the slurped review threads

Round 2 of the review on #9390 found the paginated review-thread fetch
poisons its own output on a partial page, and asked for two clarifications
around it.

R2-C (Critical) — on a failing page gh skips `--jq` and appends that page's
raw response body (a rate-limit message, or a GraphQL error envelope) to
stdout after the good nodes. The unfiltered `jq -s '.'` slurped it as an
extra element, and both consumers below iterate `.comments.nodes[]` over
every element, so the first one exited 5. This step runs under errexit, so
that aborted 'Push and report' AFTER a good push had landed — the report and
the markers were skipped and the job failed. That contradicts the two
invariants the block documents: a resolve failure must never fail a good
push, and a partial fetch is used rather than discarded. The slurp now keeps
only thread-shaped documents.

R2-1 — the comment block warned against adding `endCursor` to the inner
`comments` pageInfo, but the outer pageInfo's field ORDER is load-bearing for
the same reason: gh's cursor scanner carries its flags across pageInfo
objects and breaks at the first one yielding both fields, so alphabetizing to
`pageInfo{endCursor hasNextPage}` stops after page one just as silently. Said
so at the query, and at the test pin that goes red on a reorder, so the pin
is understood rather than bumped.

R2-2 — the stderr fold dropped the `tr '\r\n' '  '` that its ten sibling
sites apply. Actions parses workflow commands line by line and gh's
secondary-rate-limit stderr spans two lines, so the annotation kept only the
first — cutting off the words that separate a back-off from a credential
rotation.

Verification: `scripts/tests/qwen-autofix-workflow.test.js` 179/179; yaml
parses; eslint and prettier clean. Mutation-checked all three: reverting the
slurp filter fails the resolve arm with exit 5 (expected 5 to be 0),
reordering the outer pageInfo fails the field-order pin, and dropping the
`tr` fails the folded-reason arm on the second stderr line.

* docs(autofix): correct the field-order comment's mechanism (#9390 R3-1)

The comment explaining why `pageInfo{hasNextPage endCursor}` order is
load-bearing described the silent stop as happening with the carried
`hasNextPage` "already true" from the last inner page. That cannot
produce the symptom: gh's `findEndCursor` returns a cursor only `if
hasNextPage`, so a carried true would keep the walk going.

The real mechanism is the opposite one. The scanner carries its flags
across `pageInfo` objects and breaks at the first point both have been
seen; under `pageInfo{endCursor hasNextPage}` that break lands on the
outer `endCursor` while `hasNextPage` still holds the last INNER page's
value — almost always false, since thread comment pages rarely truncate
— and the outer page's own `hasNextPage` is never read. gh returns no
cursor and the walk stops after page one, exit 0 and silent.

Reworded in both places the clause was copied to: the workflow comment
and the field-order pin's comment in the test. No assertion, no shell,
and no query text changes; `pageInfo{hasNextPage endCursor}` and the
test that pins it are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(autofix): keep gh's thread-fetch stderr off a predictable WORKDIR path

R4-1 (Critical, #9390): the review-thread pagination wrote gh's stderr to
`${WORKDIR}/threads-fetch.err` and read it back with `tail -c 300`, both
without a file-type guard. WORKDIR (`/tmp/autofix-review-<pr>`) is
bind-mounted read-write into the agent docker sandbox, and the round that
just finished ran branch code inside that sandbox, so the name is
attacker-chosen by the time this step runs.

A planted FIFO makes bash block on the O_WRONLY open before gh even execs,
and the only reader is the `tail` that runs strictly after gh returns — so
the step hangs to the job timeout with the push already landed, losing the
report comment and the round markers. That breaks the invariant this block
states for itself: a resolve failure must never fail a good push. A planted
symlink instead turns the redirect into a truncate/write against the link
target and the tail into a 300-byte arbitrary-file read folded into a public
`:⚠️:`.

Route the stderr through a fresh `mktemp` regular file instead, matching the
`gh api user` checks elsewhere in this workflow, and remove it afterwards.
The diagnostic is unchanged: the warning still carries gh's own reason, which
is the only text separating a transient rate limit from an expired PAT.

Test: plant a symlink at the old path, run the block through a failing fetch,
and assert the target's bytes are neither overwritten nor folded into the
annotation; plus assert the named path is not created at all. Mutation-
verified — restoring the `${WORKDIR}` redirect turns the canary assertion red
(`expected 'threads-fetch stub failure' to be 'CANARY-MUST-SURVIVE'`). The
FIFO half cannot be written as a plain assertion because the pre-fix code
hangs rather than fails; the same "named path is never opened" property
defuses it.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 01:32:46 +00:00
Shaojin Wen
bc2d205d29
refactor(ci): simplify the review checkout self-heal back to wipe-and-retry (#9327)
* refactor(ci): simplify the review checkout self-heal back to wipe-and-retry

#9220 fixed a real incident (a corrupt persisted workspace made seven
review jobs fail checkout on the same missing SHAs), but eight review
rounds grew the heal step from ~15 lines into ~60 lines of path-guard
layers (realpath canonicalization, two trailing-slash strip loops, a
denylist case, a RUNNER_WORKSPACE allowlist) plus ~450 lines of tests
pinning their mutation resistance.

Every removed layer defended against a mangled GITHUB_WORKSPACE. That
variable is set by actions/runner; anything that could mangle it — a
compromised runner, a step writing GITHUB_ENV — already executes
arbitrary code on the machine and needs no wipe to do damage, so the
guard cannot defend against the only actor able to trigger it. The
realistic contract is the :? guard: fail loud on a dropped variable.

Kept and still pinned by tests: the pool wipe idiom, the sudo fallback
leg (exact argv), the never-fail exit contract with named survivors,
the identical retry checkout, and the continue-on-error invariants.
Also dropped with the guards: the GNU-only realpath flag and its
host-probe test machinery.

* test(ci): pin the runner-owned GITHUB_WORKSPACE premise before the workspace wipe

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(ci): seal the live override channels into the review wipe step

* fix(ci): refuse a redirected workspace and pin the clean-wipe silence

Addresses the two doudouOUC findings on the simplified heal:

- The wipe now validates the filesystem OBJECT at $WS, not just the
  string: find -P does not descend a symlinked start, so a redirected
  workspace logged 'wiped for a clean retry' while deleting nothing,
  and the secret-bearing review step would then run through the
  redirection. Refuse loud on a symlink or non-directory — POSIX-only,
  no false-positive surface (a legitimate workspace is always a
  runner-created plain directory), and it pins the only sudo-escalated
  wipe in the pool to a validated target.
- The clean-wipe silence branch was unpinned: the reviewer's minimal
  mutant (dropping the if/fi pair) shipped an empty-list survivor
  warning on every heal with the suite green. The clean-wipe test now
  asserts the success annotation and the absence of the survivor
  warning; both mutants verified red.

* test(ci): seal the wipe step's surviving override channels

* test(ci): seal the wipe's surviving override channels, pin its signals

Addresses the open review findings on the simplified heal:

- The seal's premise covered declarative env, $GITHUB_ENV/$GITHUB_PATH
  run writes, and the pre-wipe action set, but three channels passed it
  unchecked: a wipe-step `shell:` or workflow/job `defaults:` wrapper
  re-targets the environment at exec time; SHELLOPTS rides the same
  bash-startup family as BASH_ENV/ENV yet sat outside the dangerous
  name class; and ACTIONS_ALLOW_UNSECURE_COMMANDS re-enables the legacy
  ::set-env:: / ::add-path:: spellings the run-text scan did not match.
  Each channel was reproduced green against the old seal (mutant probe)
  and now turns it red.
- The both-legs-fail test now also pins the else-branch "could not
  wipe" warning, and a dedicated test pins the `[ ! -d ]` refusal for a
  nonexistent workspace — the plain-file test alone still passes a
  guard mutated to `[ -f ]`.
- The non-sudo wipe leg keeps its stderr: the 2>/dev/null discarded
  exactly the diagnostics oncall needs when the wipe fails, and the
  sudo leg already ran unsuppressed.

* fix(ci): refuse workspace wipe through symlinked path components (#9327)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-19 00:56:21 +00:00