* perf(ci): optimize autofix pipeline — fast-track, skip duplicate build, scoped tests (#6196)
Three optimizations to reduce autofix wall-clock from ~48 min to ~28-35 min:
1. Fast-track decision: skip LLM assessment for trusted triggers
(workflow_dispatch with explicit issue, issues:labeled event).
2. Skip duplicate build: set QWEN_SKIP_PREPARE=1 so npm ci does not
re-run the prepare hook that builds+bundles before the explicit
build step.
3. Scoped verification tests: run only changed-file tests via
--changed instead of full per-package suites. Full regression
is covered by regular CI on the PR.
Also hardens live test gating (require QWEN_CODE_RUN_LIVE_TESTS=1)
and increases crawler test timeout to 30s.
* refactor(ci): simplify verification gate package discovery
Replace 30-line inline Node script with the original one-liner
`grep -oE '^packages/[^/]+'`. The Node script checked for
package.json existence and test scripts, but `--if-present` already
handles missing test scripts and all workspace dirs have package.json.
Addresses design-review item #7.
* fix(ci): tighten autofix changed-test gate
* fix(ci): require maintainer-applied `autofix/approved` label for tier-1 fast-path
The scheduled scan and `issues:labeled` event paths in `qwen-autofix.yml`
previously trusted the `status/ready-for-agent` label alone, which is
applied by an LLM reading untrusted issue text. An attacker could craft
issue content to trick the triage LLM into labeling it ready, bypassing
human review.
Introduce an `autofix/approved` label that only maintainers (write+
permission) can apply. Both the cron scan and event-triggered paths now
require this label alongside `status/ready-for-agent` before entering the
autonomous fix pipeline (dual-factor: LLM signal + human confirmation).
Also:
- `release.yml` automatically adds `autofix/approved` on workflow-created
release-failure issues (trusted source, preserves existing automation)
- Forced-issue path (non-dispatch) also checks for the new label
- Claim step strips `autofix/approved` to keep processed issues clean
Closes#5634
* fix(ci): address autofix approval review
* fix(ci): address autofix approval follow-ups
* test(ci): cover autofix approval gate
* fix(ci): harden autofix approval revalidation
* fix(ci): avoid autofix approval retry loop
* fix(ci): harden autofix claim handling
* fix(ci): clarify autofix reapproval label
* fix(ci): make autofix claim label update non-destructive
The macOS audio prebuild job now packages both arm64 and x64 slices from the macos-14 runner, but the artifact name still advertised only arm64. Add a matrix suffix so that upload-artifact names the macOS bundle accurately while leaving other platform names unchanged.
Constraint: Existing collect job downloads prebuilds-* and should not need a pattern change
Confidence: high
Scope-risk: narrow
Tested: actionlint v1.7.7 .github/workflows/audio-capture-prebuilds.yml
Tested: node YAML parse for .github/workflows/audio-capture-prebuilds.yml
Tested: git diff --check
git diff --quiet exits 0 when only CRLF normalization differs (content
is identical after filter), so the conditional git restore was skipped
even though the working tree was dirty. This caused git checkout -B to
fail on NOTICES.txt. Remove the conditional guard so restore always runs.
Co-authored-by: Qwen Autofix <qwen-autofix@alibaba-inc.com>
These jobs transitively depend on precheck-pr via authorize. precheck-pr
is intentionally skipped for same-repo PRs (only runs for forks). Without
always(), GitHub Actions' default behavior propagates the skipped upstream
job, causing delay-automatic-review to be skipped even when authorize
succeeds. This breaks the entire review chain for same-repo PRs on
opened/synchronize events.
Fixes PR #5629 review not triggering.
* fix(ci): allow triage for fork PRs that pass safety precheck
Fork PRs by non-write authors were blocked from triage even when the
safety precheck passed, because the authorize job gates on write
permission. Triage is read-only (checks out trusted base code, reads PR
via API), so precheck pass is sufficient to run it safely.
Split the pull_request_target and issue_comment authorization paths:
- pull_request_target: accept write permission OR precheck pass
- issue_comment /triage: still require write permission on the commenter
tmux-testing is unchanged — it executes PR code and must keep the write
permission gate.
* fix(ci): allow prechecked fork PR automation
* docs(ci): clarify PR review authorization routing
* test(ci): guard fork precheck authorization
* fix(ci): tighten fork precheck review guards
* test(ci): guard review-request authorization
* fix(ci): use CI_BOT_PAT for precheck comment on fork PRs
The precheck workflow's "Upsert/Clear manual approval comment" steps
fail with HTTP 403 on fork PRs because `github.token` from
`pull_request_target` via `workflow_call` lacks sufficient permissions
to read/write issue comments.
- Use `secrets.CI_BOT_PAT || github.token` for the comment steps
- Add `secrets: inherit` to both caller workflows so the reusable
workflow can access CI_BOT_PAT
- Drop the `.user.login == "github-actions[bot]"` filter from the jq
query since the HTML comment marker is sufficient and the posting
user may differ depending on which token is used
* fix(ci): scope precheck comment dedup to known bot users
Narrow the jq filter to match comments from github-actions[bot],
qwen-code-ci-bot, or any Bot-type user — prevents a manually posted
marker from hijacking the dedup logic.
* ci: pass only precheck bot token
* ci(workflows): remind authors not to force-push active PRs
Add a workflow that detects force-pushes (rebase/amend/reset) to open PRs
via the pull_request_target synchronize event and posts a one-time,
bilingual reminder that force-pushing invalidates existing review comments
and that the integration bots squash all changes into a single commit
automatically. A normal push (compare status "ahead") is ignored; the
reminder is posted at most once per PR, bot-initiated pushes are skipped,
and a failed compare is treated conservatively (no comment).
* ci(workflows): address review — add issues:write, serialize without cancel
- Add `issues: write`: the listComments/createComment calls go through the
Issues API; declaring it matches the repo's other PR-commenting workflows
and avoids any risk of a 403 making the workflow inert.
- Set `cancel-in-progress: false`: an in-flight run that already detected a
force-push must finish and post. The concurrency group still serializes
runs per PR, and the once-per-PR marker prevents duplicates, so later
pushes queue and then no-op instead of cancelling (and silently dropping)
a pending reminder.
* ci(workflows): harden force-push detection per review
- Marker dedup now requires the comment to be from github-actions[bot], so a
user pasting the marker string into a comment can't suppress reminders.
- Skip known automation logins (qwen-code-dev-bot et al.) that push via PAT as
sender.type 'User', not just GitHub App bots (mirrors qwen-autofix KNOWN_BOTS).
- Narrow the compare catch to 404 (orphaned old tip -> skip); rethrow other
errors so auth/rate failures go red instead of silently no-op'ing.
- Wrap createComment with structured error logging + rethrow.
Kept 3-dot compare and base-repo owner: verified that 3-dot returns
diverged/behind for force-pushes and that the base repo resolves fork-PR
commits, while the suggested 2-dot syntax 404s in the REST API.
* test(ci): add structural test for the force-push reminder workflow
- Add scripts/tests/pr-force-push-reminder-workflow.test.js (runs under
test:scripts, which CI chains into test:ci). It asserts the trigger, repo
guard, permissions, serialized concurrency, KNOWN_AUTOMATION sync with
qwen-autofix, the 3-dot compare on the base repo, 404-vs-rethrow, the marker
author check, and the bilingual body — locking in the reviewed behaviors.
- Wrap the listComments paginate call in the same core.error + rethrow the
other two API calls already use.
- Note that KNOWN_AUTOMATION must stay in sync with qwen-autofix.yml KNOWN_BOTS.
* ci(workflows): drop concurrency group, rely on marker for idempotency
A concurrency group keeps at most one pending run per group, so a burst of
pushes can cancel a still-pending force-push run before it reaches the script,
dropping the reminder this workflow exists to post. Remove the group entirely:
every synchronize event now runs independently and is always evaluated, and the
once-per-PR marker provides idempotency. A rare double-post on two
near-simultaneous first force-pushes is the acceptable cost of never silently
missing one. Update the structural test to assert there is no concurrency block.
The reviewer's suggested `queue: max` is not a valid GitHub Actions concurrency
key (only `group`/`cancel-in-progress` are allowed) and fails actionlint.
* test(ci): use Qwen Team header and assert the dedup skip path
- Switch the copyright header to the prevailing `Qwen Team` (14 of 17 sibling
test files use it; this file had copied an older Google LLC header).
- Assert the idempotency skip log line so removing the marker guard fails a test.
* test(ci): mechanically enforce KNOWN_AUTOMATION sync with qwen-autofix
Read qwen-autofix.yml's KNOWN_BOTS and assert each login is also skipped here,
so adding a bot there without updating this workflow fails the test instead of
silently drifting. Replaces the hardcoded login list whose comment overclaimed
that the sync was verified.
* ci(autofix): loosen issue candidate filters so the agent finds work
The scheduled issue phase had been finding 0 candidates on every run. Tier-1
requires the rarely-applied status/ready-for-agent label, and tier-2's
"unattended = no human comment at all" rule fought its ">2 days old" age
gate: on a busy repo every aged bug already has a community reply, so the
funnel collapsed (128 aged bugs -> 0 survivors) purely on the comment rule.
- Unattended now keys off maintainer engagement (OWNER/MEMBER/COLLABORATOR
comments, excluding our own bots), not any human comment. Community
"+1"/"me too" no longer disqualifies an issue.
- Tier-1 and tier-2 are always both gathered (tier-1 prioritized, then
de-duped by number) so a single weak ready-for-agent issue can't starve
the run.
- Tier-2 scans a bounded age window via MIN_ISSUE_AGE_DAYS..MAX_ISSUE_AGE_DAYS
(1..15) to focus on fresh, settled bugs instead of an unbounded set.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(ci): preserve autofix issue tier priority
* fix(ci): keep ready autofix issues in tier one
* fix(ci): reserve autofix tier two slots
* fix(ci): tolerate tier two autofix scan failures
* fix(ci): tolerate tier one autofix scan failures
* fix(ci): harden autofix candidate merging
* fix(ci): harden autofix issue scanning
* fix(ci): harden autofix diagnostics
* fix(ci): skip unsafe autofix comment fallbacks
* fix(ci): drop unused autofix comments payload
* test(ci): strengthen autofix comment refresh guard
* test(ci): satisfy autofix workflow lint
* test(ci): tighten autofix tier two assertion
* test(ci): cover autofix tier merge guards
* test(ci): cover autofix unattended filter
* fix(ci): summarize autofix comment refresh drops
* fix(ci): bound autofix tier-2 comment-refresh API calls
Cap the tier-2 issues fed into the comment-refresh loop to 15 (a margin
above the ~10 ever selected, since filter_unattended_candidates drops
attended issues afterward) and request full pages with per_page=100, to
avoid GitHub secondary rate limits. Add workflow tests asserting the
forced-issue skip/in-progress exclusion and the tier-2 ready-for-agent
label exclusion stay in place.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(ci): refresh all scanned tier-2 autofix issues, not just first 15
The previous change capped comment-refresh to the first 15 tier-2 issues
before filter_unattended_candidates runs. The scan still fetches 30, so
valid unattended bugs at positions 16-30 were never refreshed nor
filterable: if the first 15 were all maintainer-attended/dropped, the run
could end with zero candidates again — the failure this PR set out to fix.
Drop the .[0:15] refresh cap and refresh the full scanned set (already
bounded by --limit 30 on the scan). per_page=100 stays as the
pages-per-issue rate-limit mitigation, and the drop summary total/counters
reflect the full refreshed set again. Top-N selection after filtering is
unchanged.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(ci): order tier priority before recency in autofix assess prompt
The assess prompt gave two conflicting selection rules: the tier note says not to pick a tier-2 issue over a comparably actionable tier-1 one, while the recency tiebreaker says to prefer the most recently reported. Since tier-1 has no age bound and tier-2 is the recent (1-15 day) tier, these point in opposite directions for comparable confidence. Make recency apply only after the tier preference and only within the same tier, removing the contradiction. Prompt text only; no shell/jq change.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(ci): guard forced-issue jq and derive autofix comment scratch path
Address two review suggestions on the autofix candidate scan step.
- Wrap the forced-issue candidate jq in the same error guard the other
jq calls use (tier-1/tier-2 stamp, tier merge): on malformed
forced-issue JSON it now emits a :⚠️: and falls back to an empty
candidate list instead of silently producing an empty candidates.json
with no diagnostic.
- Make refresh_issue_comments reuse-safe by deriving its NDJSON scratch
path from the output_file parameter (${output_file%.json}.ndjson)
instead of hardcoding tier2-with-comments.ndjson, so a second call or a
different tier can't clobber a shared scratch file. Behavior for the
current single call is unchanged.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <noreply@qwen.ai>
Large PRs with extensive diffs across multiple packages can exceed
the 85-minute effective review window (90 - 5 buffer). The recent
run #28318973107 timed out on PR #5777 which touched acp-bridge,
serve, config, and chrome-extension packages.
Bump both the job timeout-minutes and the validation cap to 120 so
the review agent has enough headroom for complex multi-package PRs.
* Upgrade GitHub Actions to latest versions
Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com>
* ci: restore ratchet annotations on upgraded actions
The action version bumps had replaced '# ratchet:owner/repo@<ver>'
annotations with bare '# vX.Y.Z' comments, which silently breaks
ratchet's ability to track and update these SHA pins. Restore the
annotations on every changed line (updated to the new major), matching
each line's pre-existing convention on main.
---------
Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: yiliang114 <effortyiliang@gmail.com>
* feat(cli): add /model --vision for a fallback vision model
Add `/model --vision <id>` and an interactive vision-model picker to
configure an image-capable model the vision bridge borrows when a
text-only main model receives an image, mirroring the `/model --fast`
pattern (flat `visionModel` setting + runtime `setVisionModel`). An
explicit vision model takes precedence over same-provider auto-select and
may cross providers; an unreachable pin falls back to auto-select rather
than firing the bridge at a model it can't reach.
Also fixes two vision-bridge issues surfaced while verifying #5126:
- The gate read stale modalities after a runtime model switch, so an
@image sent right after switching to a multimodal model could still
trigger the bridge. Refresh model-derived modalities on the qwen-oauth
hot-update paths (setModel default-model + handleModelChange) so the
gate reflects the current model.
- The transcription was appended after a now-empty "Content from <file>:"
header, leading the primary model to re-read the image file and ignore
the transcript. Stand the transcription in the image's slot and tell the
model the image cannot be read by a tool.
Closes#5597
* fix(cli): add vision model description translations
* feat(cli): warn on non-image-capable /model --vision pin; cover modality refresh
Self-review follow-ups on the /model --vision work:
- Warn (but still honor) when /model --vision or the picker pins a model that
isn't image-capable, reusing the vision bridge's isImageCapable so the
explicit and auto-select paths agree on what counts as vision-capable.
isImageCapable is now exported for the CLI to share.
- Exclude fastOnly models from --vision validation so it matches the
completion filter.
- Add a Config-level test asserting handleModelChange refreshes modalities on
the qwen-oauth hot path (the vision-bridge gate fix that lacked direct
coverage).
* fix(cli): translate the non-image-capable vision model warning
Add zh/zh-TW/en entries for the /model --vision picker warning shown when a pinned model isn't image-capable, matching the existing vision-string translations.
* fix(vision-bridge): stop showing the image transcription twice
The vision bridge printed the full transcription in its notice AND fed it to
the primary model, which then re-stated it — so the user read the same image
description twice (once in the notice box, once in the answer).
- The notice now shows only the egress-disclosure header; the transcription is
fed to the primary model and surfaced in its answer, so it is no longer
duplicated. Removes the now-unused transcript truncation / control-char
stripping helpers.
- The bridge model is instructed to transcribe/describe, not answer the user
request (the user's intent is carried as a focus hint, not a question), so its
output is context for the primary model rather than a second competing answer.
* fix(cli): register vision model i18n strings and cover /model --vision
- Register 'Vision Model' / 'Select Vision Model' in en/zh/zh-TW so the
vision picker and confirmation aren't English-only for non-English users
(mirrors the existing Voice Model entries).
- formatNonVisionModelWarning reuses the dialog's already-registered t()
key instead of a plain English literal, keeping both paths i18n-consistent.
- Add the missing bare '/model --vision' tests (interactive dialog +
non-interactive current-model report), mirroring the --voice coverage.
* refactor(vision-bridge): remove dead VisionBridgeResult.transcript field
The inline transcript display was dropped in 14de33333 (it duplicated the
description already fed to the primary model), leaving `transcript` set on
the result but never read in any production path. Remove the field and its
now-redundant test — the untrusted-wrapper assertion it carried is already
covered by 'converts images to an untrusted text block on success'.
* test(cli): cover vision-model branches; dedupe fast/vision helpers
Refactor (behavior-neutral):
- modelCommand.ts: fold identical formatUnavailableFast/VisionModelMessage
bodies into formatUnavailableAuxModelMessage(label, hint).
- ModelDialog.tsx: extract the fast/vision selection-key parsing into
encodeAuxModelSelector(); voice keeps using the model id directly.
Tests:
- config: malformed pin (catch branch) falls back instead of throwing;
authType-qualified visionModel binds to the matching provider only;
setVisionModel('')/undefined clears back to auto-select.
- modelCommand: authType-scoped vision rejection message.
- ModelDialog: non-image-capable pin emits the warning in history.
* test(cli): vision-mode coverage + register --vision completion i18n key
- i18n: register the `--vision` completion description key in en/zh/zh-TW
(was wrapped in t() with no entry, so non-English showed English).
- useModelCommand: test vision mode opens + suppresses fast, resets on close.
- modelCommand: test --vision rejects fastOnly/voiceOnly models.
Also corrects the stale "30-minute" wait-timer comment in
qwen-code-pr-review.yml (actual env timer is 10m; point to repo settings).
* fix(core): exclude primary model from explicit vision bridge selection
resolveVisionModelSelection lacked the m.id !== primary guard that the
auto-select path (selectVisionBridgeModel) already has, so pinning the
text-only primary via /model --vision would fire the bridge at a model
that cannot read images. Mirror the auto-select guard.
* chore(core): log vision bridge transcription for traceability
The transcription is no longer surfaced to the user or carried on the
result; log it at debug so a wrong primary-model answer can be traced to
a misread image vs a downstream hallucination.
* test: cover vision model helpers and vision-model dialog dispatch
Add direct unit tests for encodeAuxModelSelector (3 key shapes) and
isImageCapable (isVision/modalities/name-fallback), and a dispatch test
for the /model --vision dialog case mirroring voice-model.
* fix(cli): render vision bridge notice as a dim tip, drop doubled marker
The success/cancelled vision-bridge notice was added as MessageType.INFO
(primary-color `●` gutter) while its text also led with `🔎`, producing a
doubled `● 🔎` marker. Route success/cancelled notices to a new dim
`vision_notice` variant (secondary color, single `🔎` gutter glyph) and
strip the baked-in glyphs from the notice text so the renderer owns the
prefix. Failures keep the prominent red `✕` ERROR variant (also
de-duplicated).
* test(cli): assert VISION_NOTICE type for mid-turn vision bridge notice
* test(cli): cover vision_notice classification and rendering
* fix(vision): harden vision-bridge model selection + UX
Address review feedback on #5778:
- config: provider-aware isCurrentPrimaryModel() so a cross-provider namesake
(e.g. anthropic:shared-model vs an openai shared-model primary) stays an
eligible vision pin; resolveVisionModelSelection now also filters fast/voice-
only models (a settings.json pin bypasses the slash command) and warns on
every silent-drop path so a stale pin is debuggable.
- modelCommand/ModelDialog: reject pinning the current primary as the vision
model at set time (ignored at runtime) instead of persisting a dead pin.
- useModelCommand: vision mode suppresses voice so the dialog can't end up in
two specialized modes at once.
- HistoryItemDisplay: vision_notice gets 0 top margin like its notice peers.
- vision-bridge-service: log transcription metadata only (model + length),
never the raw text — screenshots carry tokens/PII and debug logs can be
shared in support bundles.
PR #5778.
* fix(vision): fail-closed bridge routing, ambiguity guards, i18n, settings, tests
Address the remaining #5778 review feedback:
- baseLlmClient/sideQuery/vision-bridge: add an opt-in `failClosed` to the
per-model resolve path; the vision bridge sets it so a missing cross-provider
credential fails the conversion instead of silently routing image payloads at
the text-only main generator (which BaseLlmClient otherwise falls back to).
- ModelDialog: reject a vision pin that maps to multiple same-provider endpoints
(the persisted authType:modelId can't disambiguate base URLs), mirroring the
voice-mode duplicate guard.
- modelCommand: wrap the non-interactive vision status message in t() with
en/zh/zh-TW locale entries.
- SettingsDialog: wire the Vision Model row into the sub-dialog dispatch so Enter
/ Right-Arrow open the picker (like fastModel).
- image-part-utils: document the multi-image drop / "don't read_file" contract.
- tests: fail-closed resolve paths, primary-guard fallback, vision initialIndex,
authType-qualified + malformed vision selectors, failure-path image removal.
PR #5778.
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: yiliang114 <effortyiliang@gmail.com>
* feat(cua-driver): vendor trycua/cua driver with 1000-normalized coordinate support
Vendor libs/cua-driver from trycua/cua into packages/cua-driver as the
basis for qwen-code's computer-use backend, adding an opt-in relative
(1000x1000 normalized) coordinate mode for Qwen-VL clients.
- coord_norm.rs: 0-1000 <-> pixel conversion, per-(pid,window_id) size
cache, tools/list description rewrite (TDD, 27 tests)
- ToolRegistry: normalized field + invoke input/output hooks
- protocol.rs: system-instruction coordinate wording switched by mode
- serve.rs: daemon list path description rewrite (input_schema aware)
- main.rs: CUA_DRIVER_RS_COORDINATE_SPACE env seed
Default coordinate_space=pixels => zero behavior change for existing
pixel clients. Set CUA_DRIVER_RS_COORDINATE_SPACE=normalized_1000 to
enable. Excludes rust/target build output.
* feat(cua-driver): make normalized coordinate scale configurable
Add CUA_DRIVER_RS_COORDINATE_SCALE (default 1000) so the normalization
full-scale can absorb the Qwen 999-vs-1000 cookbook ambiguity without a
recompile. norm_to_px/px_to_norm now take an explicit scale; denormalize_args
reads the process-wide COORDINATE_SCALE seeded once at startup from env.
* ci(cua-driver): add cross-platform release workflow for vendored driver
Standalone GitHub Action that builds, signs, and releases the vendored
cua-driver under packages/cua-driver. Adapted from upstream trycua/cua
cd-rust-cua-driver.yml:
- macOS: universal binary (lipo arm64+x86_64), codesigned + notarized into
CuaDriver.app using qwen-code's existing secrets (MAC_CSC_LINK cert +
App Store Connect API key notarization); Developer ID identity is
auto-discovered from the imported cert.
- Linux: x86_64 + arm64, built in debian:11 for a glibc 2.31 floor.
- Windows: x86_64 + arm64, unsigned (no EV cert, matches upstream).
- Release: softprops/action-gh-release on cua-driver-rs-v* tags or manual
dispatch, prerelease.
Triggered by tag push (cua-driver-rs-v*) or workflow_dispatch.
* chore(cua-driver): rebrand vendored driver as qwen-cua-driver
Rename the vendored trycua/cua driver so the fork installs and runs
independently of any upstream trycua install:
- binary cua-driver -> qwen-cua-driver
- bundle CuaDriver.app -> QwenCuaDriver.app
- bundle id com.trycua.driver -> com.qwencode.cua-driver
Updates the cargo/uia manifests, Info.plist, bundle/proxy launch paths,
permission/health-report wording, the install/build scripts, and the
cross-platform release workflow.
* feat(cua-driver): finish relative-coordinate mode — toggle, scale, zoom/move_cursor
- CUA_DRIVER_RS_COORDINATE_SPACE is now a 1/0 toggle (via is_env_truthy);
default off keeps pixel mode byte-identical to upstream.
- Thread CUA_DRIVER_RS_COORDINATE_SCALE through every coordinate surface
(was hardcoded 1000): input denormalization already used it; now the
rewritten screenshot dims, the tool/param descriptions, and the agent
instructions track the configured scale too.
- Normalize zoom (window basis) and move_cursor (screen basis) inputs and
rewrite their descriptions, alongside click/double_click/right_click/drag.
- Fix zoom on downscaled (Retina) windows: apply the get_window_state resize
ratio so the crop lands on the region the agent saw. Normalized mode only;
pixel-mode zoom unchanged.
All coordinate behavior stays gated on the normalized flag, so the default
(pixels) path is unchanged from upstream.
* chore(cua-driver): add upstream-sync script (git subtree unusable here)
`git subtree split --prefix=libs/cua-driver` hangs on a commit deep in
trycua/cua's history, so the subtree add/pull workflow isn't usable for
the vendored driver (and a pull would re-split + re-hang every time).
Add scripts/sync-from-upstream.sh instead: it git-diffs two upstream refs
(never walks the full history, so it dodges the hang), reprefixes the
libs/cua-driver delta to packages/cua-driver, and `git apply --reject`s it
on top of our local changes — conflicts land as *.rej for manual fixup.
Record the vendored version in .vendored-from and document the migration +
sync method in the design doc.
* chore(cua-driver): exclude vendored driver from qwen-code ESLint
The vendored packages/cua-driver tree carries upstream JS (e.g. the
test-harness Electron app) that doesn't follow qwen-code's lint rules and
fails CI. It is not a workspace package (no package.json) and is not
qwen-code TypeScript, so add it to eslint.config.js global ignores —
alongside packages/desktop/** — the standard treatment for vendored code.
* fix(cua-driver): let start_session revive an idle-reaped session
Ports the fix from upstream trycua/cua#2035 into the vendored driver.
When a session is reaped for idleness, a subsequent start_session with the
same id failed instead of resuming it. Revive the ended session in place so
the agent can continue rather than getting a hard error.
* fix(cua-driver): retry daemon socket writes on EAGAIN
Ports the fix from upstream trycua/cua#2036 into the vendored driver.
A non-blocking daemon socket can return EAGAIN/EWOULDBLOCK mid-write when the
peer's receive buffer is momentarily full. The driver treated that as fatal
and dropped the connection. Add a bounded retry/poll loop (mirror of the
read-side socket_io helper) so transient back-pressure no longer kills the
session; only a real timeout or hard error fails the write.
* fix(cua-driver/linux): stop reporting bare "Clicked" for X11 synthetic clicks
Ports the fix from upstream trycua/cua#2025 into the vendored driver.
On X11, clicks are delivered via XSendEvent synthetic events, which many
toolkits (GTK/SDL/Allegro) ignore because send_event is set. The driver still
reported a flat success ("Clicked"), masking that nothing happened. Report
the synthetic-delivery caveat honestly so the agent can fall back instead of
assuming the click landed.
(platform-linux crate is not built on macOS; verified by clean upstream apply
and covered by upstream + release-workflow Linux CI.)
* fix(cua-driver/windows): list empty-/null-title top-level windows
Ports the fix from upstream trycua/cua#2021 into the vendored driver.
list_windows filtered out any top-level window whose title was empty or null,
so legitimate targets (splash screens, some Electron/game windows, tool
windows) were invisible to the agent and unclickable. Include empty-title
windows, using class name / process as a fallback label.
(platform-windows crate is not built on macOS; verified by clean upstream
apply and covered by upstream + release-workflow Windows CI.)
* chore(cua-driver): track cherry-picked upstream PRs; fix vendored-from
The vendored copy is actually at cua-driver-rs-v0.6.7 (workspace version and
all 0.6.7->0.6.8 delta files confirm it), but .vendored-from had drifted to
0.6.8 during an earlier sync-script trial whose code delta was not kept. Left
as-is it would make a future sync diff 0.6.8->newer and silently skip the real
0.6.7->0.6.8 fixes. Correct it back to 0.6.7.
Also record the four not-yet-merged upstream PRs we carry as cherry-picks
(trycua/cua#2021/#2025/#2035/#2036) in .vendored-patches.md, and have
sync-from-upstream.sh point at it so the next sync reconciles them.
* ci(cua-driver): satisfy repo yamllint on the release workflow
The vendored-driver release workflow tripped 114 quoted-strings violations
under the repo's .yamllint (quote-type: single, required). Single-quote all
string scalars to match every other workflow in .github/workflows.
While reformatting, the release-notes body also got its paragraph blank lines
collapsed and still referenced the old CUA_DRIVER_RS_COORDINATE_SPACE=
normalized_1000 value — restore the blank lines and update it to the current
0/1 toggle (default 0 = off; optional CUA_DRIVER_RS_COORDINATE_SCALE=1000).
* chore(cua-driver): sync vendored driver to cua-driver-rs-v0.6.8
First real run of scripts/sync-from-upstream.sh: it 3-way-applied the upstream
0.6.7->0.6.8 delta onto our local fork. 10/12 files applied cleanly; the 2
rejects (install.ps1, _install-rust.sh) were already-applied baked-version
bumps (0.6.6->0.6.7, our copies were already at 0.6.7), i.e. no real conflict.
0.6.8 brings: Wayland input path (platform-linux), linux health_report +
overlay tweaks, a platform-macos build.rs step, and dependency bumps. Version
moved to 0.6.8 across the workspace.
Verified our work survived the sync untouched: the relative-coordinate shim
(coord_norm/protocol) and all four cherry-picked PRs (socket_io/session +
linux/windows) are intact — in particular the 0.6.8 edit to platform-linux
tools/impl_.rs landed alongside our #2025 change with no collision. macOS
cargo check + 132 core tests green. (platform-linux/windows + the binary
integration test build only on their own runners; upstream CI covers those.)
* ci(cua-driver): add a dry_run gate to the release workflow
Mirror the desktop-release / release dry-run pattern: a workflow_dispatch
dry_run boolean input (default true). The cross-platform build + package jobs
always run and upload their artifacts; the GitHub Release job now publishes
only on a tag push or an explicit dry_run=false dispatch.
Lets us rehearse the whole build/package pipeline (dry_run=true, notarize=false)
and inspect the produced artifacts without cutting a release. A branch push
(no tag, not a dispatch) likewise builds without releasing.
Both the triage job and the PR-review job run the qwen agent directly on
the persistent self-hosted ECS pool with no per-run isolation. $HOME,
/tmp and the workspace are reused between runs, so a prior run's agent
session/memory (default ~/.qwen) or leftover draft comments
(/tmp/stage-*.md, which survive git clean) can bleed into the next run.
This surfaced on #5874: its triage posted #5872's review verbatim
(wrong author, wrong approver, wrong diff), while the same run's internal
stage actually exercised #5874 — the PR id was correct, the agent state
was stale.
Point QWEN_HOME at a per-run $RUNNER_TEMP/qwen-home on the Qwen step and
reset it (plus /tmp/stage-*.md) in the pre-run cleanup, for both jobs.
QWEN_HOME relocates the entire global qwen dir (storage.ts), so this
isolates sessions/memory/temp without touching $HOME and disturbing
git/npm. The tmux-testing job is already container-isolated and unchanged.
Refs #5882
* ci(qwen-resolve): support fork PRs and slim /resolve to conflict-only
Closes the fork gap #5779/#5862 left for the maintainer /resolve command, so it
can clear merge conflicts on community (fork) PRs, and narrows the command to
exactly one job: resolve the conflict and push it back.
- Fork PRs: fetch the head via refs/pull/N/head and push the resolved branch
back to the PR's head repository (via Allow edits by maintainers) instead of
bailing as unsupported. Validated end-to-end against a fork PR.
- Conflict-only: drop the build/typecheck/lint/test gate and npm install/refresh;
keep the structural checks (markers, index, merge-tree, default-merge, scope).
Test fallout is left to the PR's own CI and follow-up tasks.
- Push-failure classification: workflow_scope (the merge carries the base's
.github/workflows/** changes, which a token without the workflow scope cannot
push), permission (403 / 404), and moved (stale force-with-lease) each get an
actionable comment; the redacted git stderr is logged for diagnosis.
NOTE: the push bot's PAT (CI_DEV_BOT_PAT) needs the `workflow` scope, since
resolving merges the base in and that update touches workflow files.
Guard tests updated; 12/12 pass.
* ci(qwen-resolve): avoid PR head ref collisions
* ci(qwen-resolve): address review comments
- workflow_scope: anchor classification on GitHub's server phrase
'refusing to allow ... workflow' instead of a loose workflow.*scope, which
the attacker-controlled branch name in git's rejected-ref echo could trip.
- prepare: bail when the head repository was deleted (null headRepository →
malformed push URL).
- moved: include the run-artifact link, consistent with the other cases.
- permission: drop 'could not read' (matched transient network errors).
The conflict-resolution agent step runs with `sandbox: true`, which on
Linux needs docker or podman to launch the sandbox. The self-hosted ECS
pool ships no container runtime, so the job died at the agent step
(exit 44, "failed to determine command for sandbox") before it could
resolve anything — see run 28158417390.
Pin the job to ubuntu-latest, which ships docker and is ephemeral (a
good fit for running the untrusted PR's build/lint/test in the
verification gate). Update the cleanup and PAT-scrub comments that
assumed a reused self-hosted workspace; both steps stay as defensive
no-ops on hosted runners.
After #5842, CodeQL and the E2E suite were the only things running on every push to `main` — the per-commit, post-merge backstop. With merges landing back-to-back, those runs (CodeQL ~30min, three E2E jobs ~25/36/36min, the latter never cancel-superseded) stacked on the scarce hosted Linux pool and were a main driver of the recent runner-saturation incident.
- CodeQL moves to its own scheduled codeql.yml (nightly + workflow_dispatch). It was never a required check and findings still surface in the Security tab, so per-commit scanning bought little. ci.yml loses its now-empty `push` trigger as a result (every remaining job is gated to pull_request / merge_group).
- E2E gets event-scoped concurrency so back-to-back pushes to `main` cancel superseded runs (only the latest tree matters and it covers every merged change), plus a nightly full regression as the guaranteed signal in case a busy merge window keeps cancelling the push run. Manual workflow_dispatch added.
The merge_group gate (ubuntu + integration + mac + win) still validates every PR before it lands; this only changes the non-gating post-merge work.
Follow-up (separate PR): alert on E2E/CodeQL failure (a deduped ci-failure issue) now that they run unattended.
* ci: route the merge queue's Linux jobs onto ECS
The merge queue runs in the base-repo context but classify_pr was PR-only, so its ubuntu_runner output was empty in the queue and the Ubuntu Test + Integration jobs fell back to the shared hosted pool — piling onto the scarce hosted Linux runners exactly when the queue is busiest. Run classify_pr on merge_group too and route both the Ubuntu gate and Integration onto the same in-repo ECS pool as PRs; the MAINTAINER_ECS_RUNNER_DISABLED kill-switch and the hosted fallback are intact, and fork PRs are unaffected.
Also extend the checkout-verification guard to the merge queue: now that the queue's Ubuntu checkout runs on ECS behind the squid egress proxy, a stale-ref checkout could silently test the wrong tree, and a wrong-tree pass in the queue would merge bad code. One sub-second merge-base check.
Routing approach carried forward from #5853 by @wenshao (closed); this adds the merge-queue checkout guard on top.
* ci: fix verify-step wiring test and guard integration_cli on ECS
The merge-queue ECS routing renamed the test job's checkout guard to "Verify
checkout includes expected head commit", but no-ak-integration-ci.test.js still
asserted the old "Verify PR checkout includes head commit" name, failing the
wiring test. Update the assertion.
integration_cli now also routes to ECS (via classify_pr on merge_group) yet
lacked the protections the Ubuntu gate has. Mirror them: fetch-depth 1 (nothing
walks history; a full clone is the heaviest transfer on the ECS runner) and the
same stale-checkout guard, keyed on merge_group.head_sha.
* ci: mirror the Node 22 version probe into integration_cli
The self-hosted Node step claimed to mirror the Ubuntu gate but omitted the
major-version probe, so a non-22 Node on the ECS runner would run integration
tests with no log signal. Add the same warning-only check.
The `triage` job was pinned to `ubuntu-latest`, so `/triage` (and the
pull_request_target / issues triage paths) competed for the GitHub-hosted
runner concurrency cap. That cap is routinely saturated by the things that
*must* run hosted — the CI macOS/Windows matrix, e2e — so triage runs sit in
the queue for many minutes while the 31-runner self-hosted ECS pool sits almost
entirely idle. Comment-triggered triage never reached ECS because its routing
keyed on `github.event.pull_request`, which is null on `issue_comment` events.
Triage is a read-only analysis agent with the same security profile as the
`review-pr` job in qwen-code-pr-review.yml, which already runs on ECS: it checks
out the trusted base repo, never executes PR code, and reaches the PR/issue
only through the API. Route it to the ECS pool with the same
`MAINTAINER_ECS_RUNNER_DISABLED` fallback to `ubuntu-latest`.
The upstream `authorize` job intentionally stays on a hosted runner: it is
secret-bearing (CI_BOT_PAT) and runs before the permission gate, so it must
remain ephemeral. Only the gated, post-authorize `triage` job moves.
Add a defensive stale-worktree cleanup before checkout, mirroring `review-pr`:
self-hosted runners reuse the workspace, and the triage agent has
enter_worktree/exit_worktree, so an interrupted run could otherwise leave a
`.qwen/tmp/*` worktree that trips the next checkout. The step is a no-op on a
fresh hosted runner.
* ci: add qwen fix conflicts command
* ci: rename conflict command to resolve
* ci: handle resolve workflow failures
* ci: simplify resolve workflow reporting
* ci: fold resolve command into PR workflow
* ci: merge resolve command into PR workflow
* ci: keep PR review workflow name
* ci: reuse PR command authorization for resolve
* ci: narrow PR command authorization trigger
* ci(resolve): fix command injection, secret exposure, and edit-scope
Addresses the remaining /resolve review findings:
- Command injection (RCE): branch refs were inlined as ${{ }} into the
verify / show-artifacts run blocks; a branch named with `$(...)` or
backticks would execute on the runner that later holds CI_DEV_BOT_PAT.
Pass refs via env: and reference only "$BASE_REF" / "$HEAD_REF".
- Secret exposure: the agent step (with OPENAI_API_KEY) could run
PR-authored npm build/lint/test and exfiltrate the key. Drop
build/lint/typecheck/vitest from the agent coreTools (the credential-free
verification gate re-runs them) and install with `npm ci --ignore-scripts`
plus explicit patch-package so PR lifecycle scripts do not run.
- Edit scope: the verification gate now fails when the agent changed any
file the base branch did not, so a prompt-injected agent cannot smuggle
edits outside the conflict set. Prompt tightened to match.
Also test only real workspaces (guard packages/channels/<name> against the
non-workspace packages/channels path).
* ci(resolve): address review feedback on /resolve command
- test: import vitest globals so the lint gate stops reporting no-undef
in scripts/tests (this was the real CI-blocking lint failure)
- prepare: trap an unwritten decision and fail closed to "failed" so an
early gh/git error still reports back instead of a silent red run
- report: make the post-push result comment best-effort so a failed
comment POST can't leave the branch force-pushed with no explanation
- refresh: npm install instead of npm ci to tolerate lockfile drift
after a package.json conflict resolution
- verify: scan only resolution-touched files for leftover conflict
markers instead of git diff --check over the whole merged range, which
spuriously failed on pre-existing base whitespace
- prompt/artifact: use ${{ env.WORKDIR }} instead of hardcoded
/tmp/qwen-resolve
- authorize: drop the issue.state==open gate that also silenced /review
on closed PRs (resolve-pr keeps its own open guard)
- coreTools: document that the specifiers are advisory, not a security
boundary (sandbox + same-repo + no agent token are)
- test: assert the resolve-pr authorization/scope guards, that the agent
step carries no GitHub token, and the dry-run/workflow_dispatch paths
* ci(resolve): fail the verify gate when post-merge package tests fail
The build/typecheck/lint checks use `if ! cmd; then echo outcome=failed;
exit 1; fi`, but the per-package test loop ran `npm run test` bare under
`set -euo pipefail`. A test failure aborted the step before `outcome=fixed`
was written, leaving OUTCOME empty so the report posted a generic "did not
complete successfully" with no mention of which package failed. Mirror the
other gates: record the failing package and set outcome=failed.
* ci(resolve): avoid splitting multi-byte chars when truncating report bodies
`head -c 2000` cuts at a byte boundary, which can split a multi-byte UTF-8
character and leave a broken tail in the posted comment. Pipe through
`iconv -f UTF-8 -t UTF-8 -c` to drop the incomplete trailing sequence.
* ci(resolve): report agent infrastructure failures distinctly in the verify gate
The verify gate runs under always(), so when the resolve_conflicts agent step
fails at the infrastructure level (API timeout, model quota, action crash) it
still executed with an unmodified branch — the merge-tree check then misreported
"branch still has merge conflicts". Short-circuit on a non-success agent
outcome and name the real cause instead.
* ci(resolve): trim verbose review-fix comments to house length
* ci(resolve): least-privilege GITHUB_TOKEN for resolve-pr job
Drop the resolve-pr job permissions to contents:read only and route every
write through an explicit PAT, so the implicit GITHUB_TOKEN that PR-controlled
build/lint/test see in this job carries no write scopes:
- job permissions: contents:read only (was +issues/pull-requests:write)
- Acknowledge reaction: use CI_DEV_BOT_PAT instead of GITHUB_TOKEN
- Verification gate: set GITHUB_TOKEN='' for the PR-controlled step
Defense-in-depth: persist-credentials:false already keeps the checkout token
off disk, so this is least-privilege hardening, not a reachable-leak fix.
* ci(resolve): fix skip-report gating, route to ECS with cleanup, harden tests
- Report skipped request: add always() so the prepare-step EXIT trap's
decision=failed is reported instead of skipped on a crash.
- Report skipped request: make gh pr comment best-effort with a ::warning
fallback, matching the Report result step.
- Route resolve-pr to the self-hosted ECS pool (matching review-pr) and add a
cleanup step that wipes stale ${WORKDIR} artifacts; these runners reuse
workspace and /tmp and the verification gate builds untrusted PR code.
- Tests: pin the four verification-gate failure strings against regression.
* test(resolve): pin verify-gate failure checks and skip-report always() gate
Address review feedback: the verification-gate test now also pins the
agent-infrastructure-failure, missing-address-summary, and unresolved-index
guards; and the failure-paths test asserts the Report-skipped-request step keeps
its always() gate so an EXIT-trap decision=failed actually reports on a prepare
crash. The best-effort skip-comment suggestion was already addressed in
03795302a.
* ci(resolve): ECS kill-switch fallback, safe HTML strip, prompt hardening, test pins
Address review feedback on resolve-pr:
- runs-on honors MAINTAINER_ECS_RUNNER_DISABLED and falls back to a hosted
runner when ECS is toggled off, matching the review path (the verification
gate runs fine on an ephemeral hosted runner — better isolation, no cleanup).
- append_safe_file strips only active-content HTML elements instead of every
`<...>`, so TS generics (Map<string, number>) and JSX in the agent summary are
no longer garbled in the posted comment (GitHub sanitizes comment HTML anyway).
- the agent prompt reads the base branch name from context.md instead of
inlining ${{ base_ref }}, keeping a branch name out of the LLM prompt text.
- document that the scope guard's file-level granularity is intentional
defense-in-depth (real containment = sandbox + same-repo + no agent token).
- test: pin persist-credentials:false, GITHUB_TOKEN:'', sandbox, and the
--ignore-scripts install guards so a future edit can't silently drop them.
* ci(resolve): harden /resolve workflow against review findings
- prepare: arm the fail-closed EXIT trap before mkdir; reject CR/LF in
write_output so an attacker-set PR title can't inject GITHUB_OUTPUT keys
- report: scrub the bot PAT from .git/config after push (self-hosted runner)
- verify: surface a failed dependency refresh instead of a stale build error
- scope guard: use -z/sort -zu like the conflict-marker check
- prompt: require a Conventional Commit so the default merge message passes
- tests: bound the resolve-pr slice; assert SHA-pinned lease + no-cancel guard
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: yiliang114 <effortyiliang@gmail.com>