Commit Graph
1266 Commits
Author SHA1 Message Date
易良andQwen-Coder 1a20792cfa ci: route the no-AK integration gate to ECS for every pull request (#16132)
* ci: route the no-AK integration gate to ECS for every pull request

Same trust class as lint_and_static (2026-11-02 extension of the
2026-10-31 decision): the integration step executes PR code, but the
lane carries zero secrets by construction — every model key is emptied
at the step and the job's only token is the read-only checkout grant.
The gate's step-level runner.temp HOME becomes run-unique and is
cleared before use (the R2-1 lesson: RUNNER_TEMP survives across jobs
on this pool). test and integration_cli keep the association-gated
ubuntu_runner. Mutation-verified: runs-on revert, fixed HOME, and a
real key in the PR-code step each red the routing battery.

* fix(ci): isolate no-AK HOME from the first step; value-pinned zero-secrets

Round-2 review findings: (1) Critical — the gate-scoped HOME missed the
lane's actual fork-code start: checkout's local action and the install
(lifecycle scripts plus scripts/prepare.js, un-skipped) ran under the
runner's real shared home with a shared COREPACK_HOME — the exact
cross-job poisoning channel the lint acceptance closed. Mirror lint:
first-step GITHUB_ENV run-unique HOME + COREPACK_HOME inside it +
shared verified XDG cache + always() reclamation. (2) Critical — the
zero-secrets guard was name-based and missed odd-named secrets; enforce
by VALUE (every non-path env key empty, no secrets.* anywhere in the
gate). (3) runs-on literal pin tightened (timeout line also mentions
lint_runner). Docs: EN single-lane phrasing, stale merge-queue claim,
2026-11-02 paragraph rewritten for the first-step shape. Six mutations
red (first-step removal, shared corepack, odd-name secret, no cleanup,
runs-on revert, fixed HOME).

* docs(ci): correct the enforcement-order claim and stale trust-basis note

Round-2 review: the doc claimed the routing battery runs before
dependency install — false (HELPER_TESTS imports yaml, runs after
install, and not at all on github_ci_only PRs' own runs); and the
ci.yml trust-basis comment still described the round-1 gate-scoped
HOME shape. State the actual timing and detection latency, and the
shipped first-step shape.

* fix(ci): scrub ANTHROPIC_AUTH_TOKEN at the no-AK gate; honest cache naming

Round-3 review: the Anthropic SDK's auth resolver back-fills
ANTHROPIC_AUTH_TOKEN from env, so a runner-account residue would survive
into fork PR code on the pool — add it to the gate's scrub list (pinned
by both suites). The 'Configure persistent npm cache' step name promised
persistence the per-run HOME removes; rename it and record the accepted
cold-install cost (shared pnpm store stays a follow-up).

* fix(ci): scrub ANTHROPIC_AUTH_TOKEN at the no-AK gate; honest cache naming

Round-3 review: the Anthropic SDK's auth resolver back-fills
ANTHROPIC_AUTH_TOKEN from env, so a runner-account residue would survive
into fork PR code on the pool — add it to the gate's scrub list. The
value whitelist only iterates present keys, so the deletion mutation
escaped it; pin the key's presence-and-empty in the battery and the
28-key literal list. The 'Configure persistent npm cache' step name
promised persistence the per-run HOME removes; rename and record the
accepted cold-install cost (shared pnpm store stays a follow-up).

* test(ci): pin the no-AK grant, HOME template and reclamation path

Three assertions in the no-AK routing battery proved less than they read:

- the job's `permissions` was never inspected, so the read-only-checkout
  grant that justifies routing untrusted fork code onto the pool could be
  widened without a red test;
- `create.includes('HOME=')` is satisfied by `COREPACK_HOME=` and
  `XDG_CACHE_HOME=`, so deleting the real HOME/USERPROFILE publication kept
  the test green;
- the cleanup only had to contain the `qwen-no-ak-home-` prefix, so a path
  that dropped `-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}` would still pass
  while removing a directory nothing used.

Pin the grant by value, the publication by its printf template, and the
reclamation by the exact run-unique path the create step builds.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw48g5gafx

* test(ci): pin the realized HOME values in the shared ungated lane loop

R2-1: assert the create `home=` line, the gate QWEN_HOME and the cleanup
command by value, and require the exact set of `$GITHUB_ENV` writers. An
`echo` stand-in for the cleanup, a `-v2` create path and an appended second
HOME writer all kept the battery green under containment checks.

R1-2 / R1-1: hoist those pins into the shared `ungated` loop so
`lint_and_static` gets them too. Its create step was pinned by
`includes('HOME=')` (satisfied by `COREPACK_HOME=`) and its cleanup by
`includes('qwen-')`, so a dropped `HOME=%s` or a retargeted `rm -rf` survived.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw4h0sl6g9

* test(ci): red on later republishes of the isolated HOME keys

R3-1: the GITHUB_ENV equality guard read step 0 only, so a later step that
republished HOME reverted the run-unique home by last-write-wins while every
assertion stayed green — the isolation would silently stop applying to the
steps that run PR code. Scan the remaining steps for isolated-key
$GITHUB_ENV writers and for step-level HOME/USERPROFILE/COREPACK_HOME/
XDG_CACHE_HOME overrides, leaving the npm cache step's own key a legitimate
later write.

Mutation-verified on this head: appending printf 'HOME=%s\n' ... to
integration_no_ak's 'Configure per-run npm cache (self-hosted)' step and
adding a step-level `env: HOME` each turn the suite red (103 pass -> 102
pass / 1 fail); reverting restores 103/103.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw4ng9wvgi

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 16:41:48 +08:00
jinyeandQwen-Coder 738c4bbf7e fix(serve): bound command snapshot replay memory (#16413)
* fix(serve): bound command snapshot replay memory

Suppress unchanged command snapshots and retain only the latest full skill details so periodic refreshes cannot grow daemon heap indefinitely. Bound replay rings by serialized bytes while preserving ACP live delivery and reconnect IDs, and expose retention diagnostics.

* fix(serve): avoid redundant replay seed serialization

Size only retained complete command snapshots when seeding replay history, and forward the replay byte limit through the development daemon launcher. Pin snapshot accounting and lifetime eviction counters with independent regression assertions, and document the count/byte retention bounds and conditional command notifications.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 16:13:17 +08:00
45f71b3714 fix(ci): apply the apt-mirror probe to the release docker lane too (#16278)
* fix(ci): apply the apt-mirror probe to the release docker lane too

The release docker integration lane builds the same sandbox image as the E2E
leg but never selected an apt mirror, and it prunes the builder cache on every
run so its apt layers are always cold. On mainland pool hosts that rebuilt
against the throttled deb.debian.org path (11-45 kB/s) — the same defect #16214
fixed for E2E, left unfixed here because it was out of scope (#16271).

Extract the probe (apt_mirror_probe_rate / select_apt_mirror) and a new
apply_apt_mirror helper into a shared .github/scripts/apt-mirror-probe.sh,
sourced by both run-e2e-tests.sh and run-release-docker-integration.sh. Each
calls apply_apt_mirror before build:sandbox, so a mainland host builds against
the VPC intranet mirror (6-9 MB/s, verified for E2E) while a healthy default is
left untouched. The Dockerfile APT_MIRROR ARG already exists from #16218.

Contract tests read the shared probe and pin the candidate loop and the source
statement (not substrings the comments also satisfy); both callers must apply
the probe before the build. Mutation-verified: removing the release call, a
candidate, or a source line each reddens the suite.

* fix(ci): carry the apt-mirror probe into the shard-retry test rig

run-e2e-tests.sh now sources its sibling apt-mirror-probe.sh at startup.
The shard-retry meta-test copies only run-e2e-tests.sh into a temp dir, so
the source resolved to a missing file and 'set -e' aborted before any shard
ran — all eight cases saw zero npm calls. Copy the probe file alongside the
script in the rig so the source resolves; sourcing only defines functions
(no network at source time) and the sandbox:none path never calls
apply_apt_mirror, so the rig stays hermetic.

Witness: CI at 315733fe showed all 8 e2e-shard-retry cases failing
(npmCalls expected 2, got 0); with the probe carried into the rig all 8
pass locally. eslint exit 0, prettier clean.

* fix(ci): address #16278 review — hermetic probe test, shebang, scope header

R1-1: the release-lane exec harness now stubs curl (per-URL rates) so the suite
no longer probes the live network; the npm stub logs BUILD_SANDBOX_FLAGS and two
execution tests assert the fast mirror is passed as --build-arg APT_MIRROR with
the CI label preserved, and that an all-slow probe adds no mirror arg. Flipping
the mirror-selection guard reddens them.

R1-2: add the inert shebang so file(1) reports text/x-shellscript and the
shellcheck lane enrols the sourced probe; a lint test pins the mime type.

R1-3: narrow the header to the two covered build sites and record the third
cold caller (release-sdk.yml build-sdk) as out of scope, tracked in #16317.

* test(ci): drive the shellcheck lane for the probe; copy scripts dir in shard harness

R2-1: the shebang test now runs the lane's own candidate selection with only
shellcheck stubbed, so a filter change dropping the probe is caught (removing the
shebang reddens it). R2-2: the shard-retry harness copies the whole .github/scripts
dir so any sourced sibling comes along instead of a hand-maintained file list.

* merge(ci): sync origin/main to pick up the pinned COREPACK_HOME gate

The lint-gate-freshness lane went red on 9b3d131cdb because main moved
.github/workflows/ci.yml at 2b9c4a98f4, and the shard-4 Linux lane died on
the very corepack 'pnpm.mjs' miss that commit fixes.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw2t0at7dy

* docs(ci): stop promising the release lane a benefit its routed host cannot get

`integration_docker` is pinned to `ecs-qwen-hk4-host`, or `ubuntu-latest` when
the ECS pool is unavailable. On the Hong Kong host `deb.debian.org` already
answers at MB/s and the VPC mirror is unreachable, so `select_apt_mirror`
correctly keeps the built-in source; on a hosted runner the same is true. The
call-site comment asserted a mainland benefit this call site cannot obtain, and
the always-cold builder cache it sits next to is unchanged.

The comment now says what the call does: apply the same probe the E2E leg uses,
so a host whose default is throttled builds against a faster endpoint, while a
host whose default is already fast keeps it. The `Fixes #16271` claim is
narrowed to `Refs #16271` in the PR body — the always-cold half stays open.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw420o4bfo

* fix(ci): keep the release docker lane script executable

The previous delivery of 6218478b62 went through the Git Data API, which
recorded .github/scripts/run-release-docker-integration.sh as 100644 while
the base has it 100755. release.yml runs these by bare path, so the mode is
load-bearing, and scripts/tests/release-workflow.test.js
> keeps the extracted release scripts executable already fails on it
(Linux unit tests shard 3/8 on 6218478b62).

Restore the recorded mode to 100755.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw48g5gafx

* test(ci): stage the probe fixture at its real path (#16278 R2-1)

The rewritten case kept its fixture at the temp-repo root, where the
lane's `^([^.]+)` candidate alternative already matches, so deleting the
`.*\.(sh|zsh|bash)` arm left it green. Put the file at
`.github/scripts/apt-mirror-probe.sh` — a leading dot makes the `.sh` arm
the only way it can be selected — teach `setup()` to create parent
directories for nested names, and assert the exact shellcheck argv.

Two-arm check on the lane's own run string: as authored, exit 0 with
`... --color=never .github/scripts/apt-mirror-probe.sh`; with the `.sh`
arm deleted, exit 1 ("matched no shell-script candidates").

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw4cqh0dg3

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-06 15:42:59 +08:00
Shaojin WenandQwen-Coder 36c8b34222 perf(core): load the MCP client's Google auth providers on demand (#16242)
* perf(core): load the MCP client's Google auth providers on demand

mcp-client imported GoogleCredentialProvider and
ServiceAccountImpersonationProvider statically, so every process that
loads the MCP client, including each resident Managed Runtime worker,
also loaded google-auth-library whether or not any server uses Google
authentication. createTransport is already async; import each provider
only for a server configured with it.

Add transport tests for the service-account impersonation provider,
which had none in this file. On Linux x86_64 an idle worker loads
0.24 MiB less JavaScript and holds 0.9 MiB less private memory.

* fix(core): ask for a restart when a Google auth provider chunk is missing

Now that the MCP client imports its Google auth providers on demand, the
bundle puts them in separate chunks. A background update that replaces
dist under a long-lived process made the next connection fail with
"Cannot find module", wrapped as a connection error for the server.

Move the restart guidance that contentGenerator already gives for its lazy
provider chunks into utils/errors (getModuleNotFoundError,
wrapLazyModuleLoadError) and apply it to both provider imports. The
original error stays as the cause.

Add google-auth-library to the ACP static closure guard, so a static
import of either provider fails check-serve-fast-path-bundle again.

* test(core): isolate the lazy Google auth chunk tests in their own file

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 18:08:58 +00:00
Shaojin Wen 5d45b42940 feat(verify-pr): give the sandboxed round the PR's review threads (#16300)
* feat(verify-pr): give the sandboxed round the PR's review threads

The sandboxed /verify round had no token and no review context. Its only
inputs were the metadata snapshot and, on a follow-up round, the previous
report. On #13652, Critical R1-26 was confirmed and then marked fixed
three days before a round that published merge-ready without mentioning
it. Eight hours later the review bot, at the head that round had
certified, still could not rule out its residual.

The verify job's trusted resolve step now writes review-threads.json
beside pr.json. It holds every review thread, resolved ones included,
since resolving is the author's claim, together with each reviewer's
standing CHANGES_REQUESTED review. It comes from one paginated GraphQL
query reshaped with jq. The snapshot is best-effort: on failure no file
is written and the step warns.

verify-pr-prepare.mjs sends the same query and writes the same shape for
the local path. It removes a previous copy first, so a failed fetch
cannot leave a stale file. A test runs the workflow's block verbatim and
prepare's transform over the same pages and requires equal output.

The skill adds report item 4, Existing review findings. Each term the
verdict turns on is computed from snapshot fields:

- a thread is blocking when its first comment is tagged Critical,
  Blocker, P0 or P1 by an owner, member or collaborator;
- a blocking thread is open when it is unresolved, or when that comment
  belongs to a standing change request, resolved or not. That is the rule
  GitHub's review decision applies.

An open blocking thread left unmeasured rules out merge-ready, and with
nothing failed the verdict is inconclusive. Findings at fail 0 would
publish as "passed with non-blocking findings". Other blocking threads are
re-measured as budget allows, and the verdict line counts those left
unmeasured. Change request bodies are listed but do not hold the verdict,
because their severities are prose. The duty is also a line in the scope
budget. A missing file reads as unknown, never as "nothing open".

The file adds 2396 bytes, so the size baseline records the exact 378242.

* test(verify-pr): pin the review snapshot's jq guard, copy order and odd answers

- The CI/local parity test runs the step's real jq program. Like the
  file's four other jq cases, it now skips when jq is missing, instead of
  failing one assertion later with a message that names neither jq nor
  the missing tool.
- The local recipe must clear the cached review-threads.json before it
  copies prepare's file. The test pinned the three lines but not their
  order. With rm after cp, every local round would read the review state
  as unknown.
- prepare's two other failure arms are now driven through the CLI: an
  answer that is not JSON, and pages without a pull request. Each must
  leave no file and say why. An empty state there would read as "nothing
  open".

* test(verify-pr): drive the review snapshot through the shared gh harness

The review-snapshot case had its own copy of the executed-step harness:
a temp dir, a stub gh on PATH and GitHub's bash flags. makeGhHarness
exists so that fidelity contract lives in one place. The factory now
records every call's argv, returns its temp dir, and has an
empty-stdout arm for paginated reads, GH_STUB_EMPTY_LIST. The snapshot
case runs through it. Its failure arms are the factory's
GH_STUB_FAIL_LIST and GH_STUB_EMPTY_LIST.

Deleting the shared stub's fail arm, its empty-stdout arm or its argv
capture turns the case red, and so does recording only the first
argument.
2026-11-05 23:50:44 +08:00
Shaojin WenandQwen-Coder edb094fea3 test(managed-agent): gate lifecycle-v2 close with a stopped worker (#16123)
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 22:22:26 +08:00
liuxuely0626andluoyun 0cc37bf444 feat: add Qwen subscription support across CLI, VS Code, and Desktop (#16378)
* feat(subscription): add development login, models and web search

* feat(subscription): refine account flows and host integration

Align desktop and VS Code subscription UI, add account logout and confirmed plan display, and map subscription thinking parameters. Remove simulation code and obsolete native authentication paths, consolidate design documentation, and cover cancellation and recovery boundaries.

* fix(subscription): preserve request errors and remove unused compatibility

* fix(subscription): refine command visibility and client logout flows

* fix(subscription): simplify account actions and desktop sign-in feedback

* docs(subscription): clarify sign-in and provider guidance

* feat(subscription): support switching accounts across clients

* fix(subscription): align account actions and open plans on demand

* fix(subscription): restore model selection after logout

Filter subscription-only commands consistently across terminal renderers, preserve model catalog error classification, and display the subscription authentication name consistently.

* fix(subscription): unify status authentication labels across clients

* fix(subscription): preserve provider connections across client logout

Restore configured providers after subscription logout across CLI, Desktop, and VS Code. Keep unrelated connections and conversation history, synchronize model state silently, and return to an unconfigured chat when no provider remains.

* fix(subscription): correct native client logout state

* feat(subscription): enable sign-in without development mode

* refactor(ui): 优化会员登录成功提示和界面交互体验

- 移除订阅欢迎提示相关状态和行为,简化 UI 状态管理
- 替换登录成功提示为四行摘要,包含邮箱、套餐、模型及常用命令提示
- 更新国际化文本,支持新的登录成功摘要内容
- 取消订阅模式下输入框下方的命令提示行,改为在登录成功摘要中展示
- 移除多余的测试用例,新增登录成功消息的单元测试校验
- 优化 AppHeader 组件,不再显示订阅启动重复提示
- 调整 Composer 组件逻辑,删除订阅提示相关副作用及渲染条件
- 修改历史消息显示组件以适应新的成功提示格式

* fix(subscription): enable Flash reasoning controls in shared UI

* fix(subscription): prioritize authorized models and simplify login dialogs

* fix(subscription): preserve gateway errors and request IDs

* fix(subscription): synchronize account state across clients

Preserve model selections after sign-out, synchronize account commands and model catalogs, and retain explicit browser sign-in flows across CLI, Web Shell, and VS Code. Cancel subscription background agents on logout and normalize expired refresh credentials without SDK error wrapping.

* fix(subscription): reset default model on explicit sign-in

Choose the default served model when explicitly signing in to Qwen Subscription, preferring Max over a previously saved Flash selection. Preserve normal startup behavior and other provider flows, and cover CLI and workspace activation with regression tests.

* fix(vscode): refresh models after successful sign-in

* fix(web-shell): hide account actions during automatic sign-in

* fix(web-shell): align model details and auth keyboard navigation

* docs(subscription): revert previous guide changes

* fix(core): reuse subscription catalog for same-login model switches

* docs(subscription): document Qwen membership sign-in

* fix(subscription): unify quota notices and preserve provider switching

Carry structured subscription quota details through chat and search errors for localized CLI, Desktop, and VS Code display. Keep other providers available when subscription discovery fails and refine sidebar account actions.

* fix(web-shell): align Chinese subscription entry title with CLI

* fix(subscription): open plans for saved non-member accounts

* fix(web-shell): show subscription commands after desktop sign-in

* fix(subscription): reauthorize explicit account sign-ins

* fix(auth): add model switching hint to subscription success

* docs(auth): list Qwen OAuth as the first option

* docs(models): simplify subscription sign-in guidance

* docs(models): remove legacy OAuth from supported auth types

* docs(subscription): link membership references and remove obsolete allowance note

* fix(subscription): preserve session until reauthorization succeeds

* fix(subscription): use pre for browser authorization and plans

* fix(subscription): use production endpoints

* test(vscode): fix editor message test formatting

* fix(subscription): preserve unauthenticated session startup

* fix(cli): classify subscription auth during ACP preflight

* fix(subscription): scope activation to the current project

---------

Co-authored-by: luoyun <[email protected]>
2026-11-05 20:44:21 +08:00
qqqys 08f1843c22 feat(mods): run the prompts a Mod submits in the Ink terminal (#16373)
* feat(mods): run the prompts a Mod submits in the Ink terminal

* test(mods): run a Mod's prompt in a real Ink terminal

* fix(mods): stop a Mod's prompts when the person stops a turn in Ink

- Esc or declining every tool a turn asks to run drops the Mods' queued
  prompts and refuses new ones until the person sends a prompt again. The
  stopped turn's turn.complete hooks run after the stop, and a call a hook
  does not await reaches the host after the hook returns, so dropping the
  queue alone let such a hook start the next turn. $.turn.abort still keeps
  the queue.
- A Monitor update waiting out its minimum interval no longer holds a Mod's
  prompt, which a Monitor printing within every interval held for good.
- A prompt put back after losing the idle moment runs once the winning
  submission settles, since that one may start no turn.
- The dual-output sidecar gets the Mod's text and a mod_prompt system
  message, as a stream-json client does, instead of the frame.
- The guide's capability table lists Ink for a Mod's turn events.

* fix(mods): drain Mod prompts after any settle and strip a stopped one

A Mod prompt queued while a submission that starts no turn held the
session (a cron's Mod command, a notification dropped before sending)
stayed queued with the session idle: the drain re-ran on settle only for
a prompt it had put back. Re-run it whenever a Mod prompt waits.

When the person stops a Mod prompt's turn before it answers, the framed
prompt stayed at the end of the model history, so the next prompt reached
the model behind it. The cancel info now says the turn ran a Mod's
prompt, and the handler strips it as it does a Goal continuation's.

* fix(mods): run a waiting Mod prompt once a Goal lets go of the turns

A Mod prompt queued while a Goal owned the session's turns waited for the
next submission to settle or another prompt to be queued. The Goal often
releases its turns after the session is already idle (its verifier accepts,
its last turn is released), so the prompt could sit until some unrelated
event. The drain now also re-runs on the Goal runtime's broadcasts while a
Mod prompt waits.

The test no longer queues a second prompt to wake the drain; it changes the
Goal snapshot and broadcasts, and fails without the fix.
2026-11-05 20:39:00 +08:00
Jiankai XiaoandJiankai Xiao de6ccb4e3c feat(serve): allow configuring the Conversations working-directory root (#16105)
* feat(serve): allow configuring the Conversations working-directory root

* test(serve): register Conversations root E2E in no-AK CI

* fix(serve): address Conversations root review feedback

* fix(serve): clarify Conversations root configuration errors

---------

Co-authored-by: Jiankai Xiao <[email protected]>
2026-11-05 16:53:05 +08:00
qqqys a818643533 feat(mods): let a Mod submit a prompt of its own in headless runs (#16302)
* feat(mods): let a Mod submit a prompt of its own in headless runs

$.prompt.submit({ text, asUser? }) raises prompt.submit through every hook
but the caller's, with the Mod as its origin, and queues what arrives
beneath them. The headless run takes it once idle and runs it as a turn of
its own: UserPromptSubmit hears it, turn.start and turn.complete fire, and
the transcript records it with the provenance mod_prompt and the Mod's
name. Unless the Mod passes asUser, the model reads the text inside a block
that names the Mod and says the person did not type it.

The call resolves once its prompt is queued, as next(e) does for a typed
prompt. The Mods' prompts start at most 8 turns before the person sends a
prompt. The terminals do not run these prompts yet: there the method is not
listed and a call resolves to a drop.

* fix(mods): queue a Mod's prompt once, outside its caller's lifetime

- Run the hooks a `$.prompt.submit` raises outside the calling hook, so a
  call the hook does not await still queues after the hook returns.
- Queue what last arrived beneath the hooks once they have all answered,
  so a hook that calls `next(e)` twice queues one prompt.
- Drop an empty or whitespace-only text, from the call or from a hook.
- Discard Mod prompts that have not started when a headless run ends, so a
  stream-json session does not run them on the client's next message.
- Escape every `<` and `>` in the framed text.
- Recall memory for the Mod's text rather than the frame, keep a Mod's
  prompt out of the session title (in its turn's place) and out of the
  commit attribution's prompt count.

* fix(mods): keep the title, SDK output and turn count right for Mod prompts

- Mark a Mod's prompt as `null` in the title projection, not `''`, so it
  no longer switches a session whose prompts recorded no display text to
  an empty projection, which stopped auto-title and `/rename --auto`.
- Tell a stream-json client of a Mod's prompt: its text as a user message,
  then a `mod_prompt` system message naming the Mod, before the turn runs.
- Reset the Mods' turn count for the person's slash commands too.
- End a Mod turn that a duplicate provider call stops as an error on the
  shared loop, as on the legacy loop and for the main turn.
- Refuse a prompt submitted while /clear or /resume ends the session, and
  start the new session with nothing queued and the count at 0.

* fix(mods): keep charging a hook while the other Mods hear its submit

A $.prompt.submit call outlives its caller, yet it lent the caller's
budget for the whole submit chain, so a hook that did not await it ran
unbudgeted until the other Mods' prompt.submit hooks answered. It now
runs as the void notices do, charging the caller as it goes.

The frame also no longer says the Mod sent its prompt while the session
was idle: most are sent mid-turn and wait for the session to be idle.

* fix(mods): run a Mod command's submitted prompt and keep it out of the title

A Mod command in a headless run always answers with a message, and that
branch returned before the drain, so a prompt the command submitted with
$.prompt.submit was discarded while the call had resolved as queued. The
reply is now shown without a turn of its own and the run goes on to the
drain, ending with one result.

A session whose prompts recorded no display text drew its title from the
conversation, which held the Mod's framed prompt. The recorder now keeps
the text the model read for each Mod prompt, and the conversation-based
title leaves those messages out.

* fix(mods): keep a Mod's prompt within its payload and its command's reply in view

- Drop a submitted prompt past half a Mod payload, from the call or a
  hook's rewrite, before it is queued, so a queued prompt is never
  reported as failed; an input too large for the bridge now rejects.
- Print a Mod command's reply in text output, and give the worktree notice
  the command would have carried to the first request the run sends.
- Leave an `asUser` Mod prompt out of the title keys, which matched a
  prompt the person typed alike, and keep the keys least recently used.
- Make Goal evidence leave a Mod's prompt out explicitly.
- Pin the documented turn limit and the tool-result guard in tests, and
  add a built-CLI test of `$.prompt.submit` against the fake server.
- Docs: six steps, the non-goal narrowed, the hook docs and the budget
  paragraph naming Mod prompts.

* test(mods): run the $.prompt.submit integration test in the no-AK job
2026-11-05 14:42:41 +08:00
Shaojin Wen ef9c891b8f fix(ci): clear the pinned corepack cache before every install (#16351)
* fix(ci): clear the pinned corepack cache before every install

The Linux test shards have been dying seconds into Install dependencies
with MODULE_NOT_FOUND on the cached pnpm's bin/pnpm.mjs since #16328
pinned COREPACK_HOME to runner.temp (main runs 41876940141, 41889761157,
41927887452; six different self-hosted hosts). The pin assumed runner.temp
is per-run, but self-hosted runners keep _work/_temp across jobs — the
lint job's isolated-HOME step already documents this. corepack trusts an
existing cache entry without re-verifying its files, so a partial pnpm
tree left by an earlier job fails every later job on that host.

Clear the pinned cache at the start of the shared install body in all
three jobs that copy it, enforcing the per-run contract the pin
documents. The guard keeps the reset a no-op where COREPACK_HOME is
unset (test_build) or already run-unique (lint). The test step still
reuses the cache install warms in the same job, and poisoned hosts
self-heal on their next job without manual cleanup.

Pin the reset and the install/test COREPACK_HOME match in the workflow
meta-test, and bump the size baseline for the added lines.

* fix(ci): scope the corepack cache reset to the shard-owned path

Review found two over-deletion shapes in the nonempty guard: a runner
that exports COREPACK_HOME would lose its entire shared cache in the
build job, and hosted lint would lose the cache its pnpm-store step had
just warmed, re-exposing the install to a registry outage. Match only
the exact shard-owned path instead; the build and lint copies become
unconditional no-ops and the three install bodies stay byte-identical.

Add an execution test that runs the shipped guard fragment under a
scrubbed environment across five cache shapes: it kills the dead-guard
mutant the string pins survived, and a widened-guard mutant dies on the
shared-cache survival cell. State the incident as observations with the
corruption source undetermined, and correct the stale per-run rationale
beside the pin.
2026-11-05 09:27:53 +08:00
Shaojin Wen 8a7c0d17d8 fix(managed-agent): find the Flyway scripts in the module jar the server ships them in (#16175)
* fix(managed-agent): find the Flyway scripts in the module jar the server ships them in

Since #15871 split the server into Maven modules, its Flyway scripts ship
in BOOT-INF/lib/qwen-managed-agent-persistence-*.jar rather than in
BOOT-INF/classes/db/. The upgrade command's check read only the classes,
so on every archive since then its database row warned "cannot compare the
Flyway scripts (no Flyway script found in ...)", and a release that changed
or reordered a script the installed database ran went unflagged.

migrationScripts() now also reads db/**/ at the root of each nested jar in
BOOT-INF/lib/. Spring Boot stores those uncompressed, so their directories
are read in place; a compressed one, which Spring Boot would not load, is
skipped. A script keeps its path as its name wherever it ships, so one moved
from the classes into a module compares as unchanged.

The reference distribution workflow now reads the scripts out of the exact
archive it built, so a later change of layout fails there rather than
turning check's row into a standing WARN.

* fix(managed-agent): read a Flyway script shipped twice with different contents as unreadable

The reader keys the scripts on their classpath path, and the nested jars
give that key a second source. Two copies of one path with different
contents left whichever the central directory listed last in the map, so
the database row compared one arbitrary copy, while the class loader would
hand Flyway the other. Matching copies are still one script; differing
ones now make the jar unreadable, which the row reports as "cannot compare
the Flyway scripts".

* fix(managed-agent): name both places that ship a differing Flyway script, and pin the local extra field

- When one script path ships twice with different contents, the error now
  names the two places, BOOT-INF/classes or the nested jar, so the
  operator need not unpack the jar to find the second copy.
- The test zips can carry a local-header extra field, as archivers add for
  timestamps or alignment, and the nested-jar case reads entries past one,
  so dropping that term from dataStart fails the suite.
2026-11-05 08:10:28 +08:00
Shaojin Wen b9e20830f7 fix(verify-pr): post flaky demotions on closed PRs and pin the blank class (#16135)
* fix(verify-pr): post flaky demotions on closed PRs and pin the blank class

The closed-PR skip from #16050 also caught the flaky variants of the
cancelled, failed-before-report and results-unavailable notices, and the
report body of a round that wrote no report.md. Those render ❌ and, on
a merged PR, name non-deterministic tests now on main, so they post
unless this run's own report already carries the demotion.

- publisher: the closed-PR skip leaves FLAKE_VERDICT=flaky alone,
  except beside this run's own report
- publisher: the gates line says its tests are "not counted
  individually above", since each suite run now counts as one
  assertion (zh: 不逐条计入上面的断言)
- checker tests: pin the blank class on their own (whitespace refused,
  U+0085 and U+FEFF accepted); before, only the shared parity table in
  the workflow suite caught a revert to trim()
- SKILL: rewrap the over-long local-path line

* fix(verify-pr): scope re-run advice to open PRs and pin the gate condition

The results-unavailable notice can now land on a merged PR, where the
verify lane refuses `@qwen-code /verify`: both variants now offer the
re-run only while the PR is open.

The flaky exemption keys on the gate verdict, which holds only because
the gate step runs on a prepared round; a test now pins that condition.

* fix(verify-pr): skip a flaky notice only beside a report that carries it

On a closed PR, a flaky notice with no live status of its own to
finalize is skipped beside this run's own report, as a duplicate of the
demotion that report carries. That report can come from an earlier
attempt whose gate did not diverge. A re-run attempt whose own live
status is missing (that post is best-effort) then lost its ❌ to the
run log, beside a report that never showed it.

A flaky verdict now adds a verify-flaky marker to the header, and the
closed-PR skip drops a flaky notice only when this run's own report
carries that marker in its header.

* test(verify-pr): pin the own-comment lookup's initializers

The publish step runs under `set -u`, and a failed comment listing or bot
identity lookup leaves OWN_ID, OWN_SUBSTANTIVE and OWN_FLAKY at their
initial values. Dropping any of those three lines kept the suite green:
no case made either lookup fail. On such a run the step would instead abort
with "unbound variable" and post nothing.

The upsert stub can now fail the listing or the identity lookup. Under
either failure, a report posts fresh rather than patching a status it cannot
see. A flaky notice on a closed PR also posts, because the lookup cannot
tell whether this run's report already carries the demotion. Deleting any
one initializer now fails the test on its own unbound variable, which the
harness now prints with the failed exit status.
2026-11-05 02:42:14 +08:00
Shaojin WenandQwen-Coder 9123d5ee04 fix(managed-agent): refuse every URL fetch refuses and say why a health probe failed (#16110)
* fix(managed-agent): refuse every URL fetch refuses and say why a health probe failed

start and verify refused a listener URL only when URL.canParse rejected
it, so a --server-url without a scheme (proxy.test:8443), with a user
name or password, or on a port the Fetch standard blocks (6000, 10080)
passed the check and then failed every probe until --timeout. They now
refuse each of those before anything runs, and a test checks that the
runtime's fetch rejects every URL the check refuses.

A probe that is sent and still fails now says what it got: start's
timeout names the last probe's answer and verify's health row names its
own, such as "HTTP 503, status DOWN", "self-signed certificate" or
ECONNREFUSED, where both said only "not UP".

The listeners() comment names the reachesHarness option verify passes.

* fix(managed-agent): refuse a Harness URL that is not http or https in upgrade start

start checked the Harness URL with new URL() alone, so an opaque value
such as proxy.test:8443 parsed with no host and no port: start then
waited out --timeout for the Harness to listen on port 80 of an empty
host. It now refuses any Harness URL that is not http or https before
it starts a unit, as the preflight would fail it later anyway.

The probe-answer test also runs the runtime's own fetch against a port
nothing listens on, so the ECONNREFUSED wording comes from a real error
rather than a constructed one.

* fix(managed-agent): keep the endpoint's answer when the deadline cuts start's last probe short

start's last health probe gets the 1 ms the deadline leaves it, so against a
real endpoint it times out before any answer arrives, and the timeout message
reported that abort instead of what the endpoint had been answering, such as
HTTP 503, status DOWN. A probe that times out on a budget the deadline cut
below the per-probe cap no longer replaces an earlier answer; one that runs
out of its own 5 seconds still does, since the endpoint stopped answering.

The test rig's fake fetch now awaits a response function, so a test can model
an endpoint that answers late and honours the abort signal. Also pins the
IPv6-zone diagnosis to a percent inside the brackets.

* fix(managed-agent): treat a health body the deadline stalls as a cut-short probe too

A health endpoint that sends its headers and then stalls the body made
response.json() reject with the signal's TimeoutError. That branch did not
report timedOut, so start's guard took the probe for an answer, and a probe
the deadline cut short replaced an earlier HTTP 503, status DOWN with
"HTTP 200, The operation was aborted due to timeout". The body-read branch
now reports timedOut as well.

The last-answer test gains two real-socket cases: a body that stalls after
its headers keeps the earlier answer, and a refused connection after an
answer replaces it, which pins the timedOut half of the guard. The design
doc says when the 5-second limit counts as the probe's own, and that any
other failure replaces an earlier answer.

* fix(managed-agent): sanitize health text and align upgrade refusals

Address review R1-1..R1-5 on the upgrade URL/health change:
- R1-1: neutralize terminal control bytes (C0/DEL/C1 -> U+FFFD) in the
  endpoint-derived health answer before it is interpolated into start's
  timeout refusal and verify's health row, so a queryable endpoint cannot
  move the cursor, split a row, or write the clipboard (OSC-52).
- R1-2: the Harness URL refusal reuses fetchRefusal when the value does not
  parse, naming an IPv6 zone exactly as --server-url does.
- R1-3: start's timeout labels the retained value 'the last answer', matching
  the design doc, since a deadline-cut probe keeps an earlier answer.
- R1-4: pin verify's URL refusal (listeners runs it with reachesHarness:false)
  with a test asserting it refuses before any probe.
- R1-5: tear down the 'closes' test server via t.after so a failing assertion
  cannot hang node --test to the job ceiling.

Tests: 63 pass; R1-1/R1-2/R1-4 mutation-witnessed (printable no-op, refusal
gated on reachesHarness, zone reason reverted each redden their pin).
Prettier and ESLint clean.

Co-authored-by: Qwen-Coder <[email protected]>

* test(managed-agent): exercise the health sanitization over a real socket

Add a real-scenario pin for R1-1: a local HTTP server serves a status field
carrying cursor moves, a newline and an OSC-52 clipboard write, and verify
runs against it through the runtime's own fetch — the real
health() -> printable() -> row() -> out() path, not the rig's fake. Asserts
the printed health row keeps no control byte while the readable text survives.
64 tests pass.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): mask a refused URL's password and name why verify's API reads failed

Found on a real systemd deployment (real control plane, Harness, MySQL,
TLS and nginx):

- A refused --server-url was printed as typed, so a URL with a password,
  which fetch refuses, put that password in the output. The refusal now
  prints it as ***.
- verify's session and turn rows still said only "fetch failed" where the
  health row above them named the cause, such as "self-signed certificate
  in certificate chain". API reads now take the same cause.
- Text the API hands back (an error reason, a body that fails to parse,
  a Session ID, an event type) reached the session and turn rows with
  its control bytes; it is now neutralized as the health answer is.

The design doc says the probe answer's control characters show as U+FFFD.

* fix(managed-agent): show a health status that is no string as its JSON

printable() ran String() on what the endpoint returned, and String() throws
on a valid JSON object whose own toString is no function, such as
{"status":{"toString":1}}. health() is not inside a try, so on a real
deployment verify exited with "Cannot convert object to primitive value"
before it printed its health row, and start's probe loop stopped at the
first probe. A value that is no string is now shown as its JSON, which is
also what the endpoint actually said; an API error reason gets the same.

* fix(managed-agent): make the upgrade reports total and bounded, and refuse only what a phase dials

Review round 5 traced its findings to two shared causes, fixed at the
source rather than one site at a time.

Endpoint text in the reports (printable, withoutCredentials):
- printable() no longer throws on any parsed JSON value: a status nested
  deeper than JSON.stringify() recurses showed a bare "Maximum call stack
  size exceeded" from verify and left start's server running without its
  guidance; it is now shown as [unprintable].
- It also stops at 200 code points, cut after the control bytes are gone
  and never inside a surrogate pair, so a long status cannot scroll the
  report away. The item count on the session row goes through it too.
- withoutCredentials() masks the whole user information, so a token in the
  user-name position is masked, and masks it textually when the URL does
  not parse. The Harness refusal masks its URL as well.

URL checks (listeners, the Harness guard):
- A phase is refused only over a URL it dials: start and verify probe the
  health endpoint, and only verify --tenant reads the API. An API on a port
  fetch refuses behind a separate management port no longer stops start.
- The Harness URL is checked as the preflight checks it, an http(s) origin
  without user information, a query or a fragment, so start refuses it
  before it restarts a running Harness rather than after.

* fix(managed-agent): take the Harness verdict from the preflight and check the API URL only when verify reads it

- verify decides whether its session row reads the API before it checks
  the listeners, so a signed deployment, or --actor without a trusted
  actor header, is no longer refused over an API URL it never dials.
- The configuration resolver now prints the preflight's own verdict on the
  Harness URL, read untrimmed as java.net.URI reads it, and start refuses
  on it before restarting the Harness: a host with an underscore, an empty
  user name or a padded value no longer restart the Harness only to fail
  the preflight.
- withoutCredentials masks everything up to the last @ of a value the
  parser rejects, so a password holding / ? or # stays hidden.
- printable counts code points in one pass and keeps only the head, so a
  huge endpoint answer costs no copy of itself.
- The request an API error names is bounded like the rest of the row.

* fix(managed-agent): print the Harness URL untrimmed, as the server parses it

A padded qwen.managed-agent.harness.base-url fails the preflight, and the
start refusal now shows the padding instead of a value that looks valid.

* fix(managed-agent): judge the Harness URL only where the server reads it

The resolver passed on the preflight's verdict on the Harness base URL
with no regard to the Harness's state, so start refused a value that a
disabled or pooled Harness leaves unread: the preflight skips its
harness row before reading it, and the server never dials it. The
verdict is now "skip" there. start refuses on the preflight's grounds
(user information, a query, a fragment, or anything java.net.URI reads
differently) only on a "false" verdict, and its own reasons only word
that verdict. It still refuses a URL that is not http or https, since it
waits for the Harness there. Without a verdict, as when the
configuration does not resolve, the preflight judges the URL once the
Harness is up.

verify now checks the type of the values the API chose before using
them. Building a route from a Session id, matching an event type, or
reading an event sequence coerced the value first, and that throws on
an object such as {"toString":1} or on an array nested deeper than the
stack, so printable() never got to show what came back. A Session
without a string id fails its row with that value, an event type that
is not a string fails the turn row, and a sequence counts only as a
number or a numeric string.

The start tests now check every run's output for the password, not
only the last run's.

* fix(managed-agent): judge a pooled Harness URL, and read only lists from the API

The previous commit gave "skip" for a pooled Harness as well as a
disabled one, since the preflight skips its harness row in both before
reading the base URL. A pooled server still reads that URL, though:
each member's QwenHostedHarnessConnector is built from it and refuses it
by the same rule as the preflight. The pool's refresh drops a member
whose connector refuses it, so such a pool accepts no Session while the
server reports UP. Only a disabled Harness gets "skip" now. A test pins
the verdict to whether the connector builds, for a single Harness and
for a pool. Since the verdict is now the server's rather than only the
preflight's, the refusal says "the server does not take".

verify reads Sessions, items and events only as lists. Data that is not
a list fails its row and says what came back: iterating it would throw,
or walk a string, and its length could be any value. A listed entry of
0, "", false or null fails the session row instead of being reported as
no Session. An event sequence moves the cursor only as a whole number,
and only forward: Number() reads "Infinity", "0x10" or " 4 " as numbers.

The user guide now says that, when the configuration does not resolve,
the preflight judges the Harness URL after the Harness starts.

* fix(managed-agent): keep start's own Harness URL rules without a verdict

When the configuration does not resolve there is no verdict. The previous
commits then let start accept any http(s) Harness URL and left the URL to
the preflight. A pooled preflight never reads it, though, and every pool
member's connector refuses user information, a query or a fragment, so
the pool would start and accept no Session. Without a verdict, start
refuses those itself again, since the server refuses them wherever it
reads the value. A "true" or "skip" verdict still asks nothing more, and
the generic wording is kept for a "false" verdict.

HarnessConfigurationTest now builds a pool through the real member
factory and shows that the single base URL decides whether a member is
accepted. The pooled half of the agreement test checked nothing the
single half did not, so it is gone.

verify now requires every page to carry its data as a list: a body that
is no object, or one whose data is null or missing, fails the row instead
of reading as no Session. A Session id of "", "." or ".." fails, since
fetch resolves dot segments and an empty id reads the list. Only
terminal: true ends a turn.

The docs no longer say the server takes only an origin, since a path is
fine. They say which component refuses the URL, and they word the
sequence rule as the code applies it.

* fix(managed-agent): sort the test's static imports, and refuse a --server-url query

The Mockito static import sat between two JUnit ones, so the verification
module's ImportOrder check failed. The static block is now sorted.

start and verify append /actuator/health and the API paths to
--server-url, so a query or a fragment there took the paths in. With
http://proxy.test/?a the command probed /?a/actuator/health, and fetch
dropped a fragment along with the path after it. Such a URL is now
refused up front.

A listed Session entry with no id is now shown as itself rather than as
"undefined". A health answer whose JSON parse error quotes control bytes
is pinned to come out sanitized.

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 01:42:38 +08:00
易良andQwen-Coder 2b9c4a98f4 fix(ci): pin COREPACK_HOME for Linux test shards on cold-HOME runs (#16328)
The Linux unit-test shards set HOME to a fresh per-run directory for
isolation. scripts/tests/corepack-warmup.js (vitest globalSetup) spawns
`corepack pnpm --version` before any worker forks, and on a cold HOME
corepack downloads the pinned pnpm from registry.npmjs.org. The ECS
egress intermittently times out on that fetch (ETIMEDOUT to 104.16.9.34),
which kills the shard after every test file has already passed — today
that produced 7 PR-facing failures across random shard numbers (2/8,
5/8, 3/8, etc.), all on the same self-hosted host.

Install dependencies runs before the test step and warms the corepack
cache under the runner's real HOME, but the test step's HOME override
makes that cache invisible. Pin COREPACK_HOME to a per-run path in both
steps so they share the same cache without touching the HOME isolation.

Per-run, not persistent: corepack does not re-verify extracted entries
on cache hit, so a shared cache would be a poisoning channel (the same
reason lint_and_static keeps its COREPACK_HOME per-run).

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-05 01:19:56 +08:00
a5fec03684 fix(ci): fail Java lanes before disk exhaustion (#16068)
* fix(ci): fail Java lanes before disk exhaustion

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): gate the MySQL pool lane instead of a hosted job

The disk floor gate landed in hosted-harness-mysql, which runs on
ubuntu-latest, so `runner.environment == 'self-hosted'` is never true
there and the step was dead code. The job that actually shares the ECS
pool disk - mysql-integration, two Maven suites plus a per-leg MySQL
container - stayed ungated.

Move the step to mysql-integration, immediately after its checkout so
the sdk-java-workflow pin still holds. This also clears the three
Hosted real-process gates pins, which reject a conditioned step among
the setup steps that precede managed_dependencies in
hosted-harness-mysql, and reflow the new test to the repo's prettier
style.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvya43o17i

* style(ci): drop the blank line left behind in hosted-harness-mysql

Moving the disk floor gate out of hosted-harness-mysql left a stray blank
line between its checkout and 'Set up Java'. Restoring it makes the whole
workflow diff additive on mysql-integration only, so 'hosted-only jobs are
unchanged' holds byte for byte and an A/B census of that job reads clean.

Co-authored-by: Qwen-Coder <[email protected]>

* ci(sdk-java): record the real workflow size after the disk-floor gates

The six #16003 disk-floor gate steps grow sdk-java.yml from 154074 to
155650 bytes. main had already drifted 3742 bytes past the recorded
150332 — inside the 4096 allowance, but it left only 354 bytes of ratchet
headroom, so this real growth tripped Lint & Static.

Record the file's actual size rather than shrinking the gates: each one is
a three-line step calling .github/scripts/check-disk-floor.sh, and the
workflow test pins all six gated jobs.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw02ewzoa2

* fix(ci): derive the disk-floor corpus from runs-on and narrow the broker claim

Address the two blocking review findings on the Java disk-floor gate:

R1-1: the meta-test skipped any job without the gate and compared a closed
six-name list, so a newly pool-routed lane that forgot the gate stayed green,
and a neutered gate (continue-on-error) kept every assertion true. Derive the
corpus from each job's runs-on (same hazard as the setup-java corpus), require
gateIndex > -1 per pool-capable lane, and assert the gate carries no
continue-on-error. The derived set is the same six jobs today. Verified by
mutation: adding continue-on-error to a gate reddens it, and renaming a gate
(ungated pool job) reddens it with 'expected -1 to be greater than -1'.

R1-2: the step comment claimed RUNNER_TEMP covers the broker store, but the
absolute free-KiB floor cannot express the broker's used-ratio refusal (a
952 GiB volume at 94% used still has ample free bytes). Narrow the comment to
the gate's real scope (near-full filesystem / inode exhaustion, #10035) and
state the broker disk-threshold investigation stays out of scope, matching the
PR description. Bump .size-baseline for the comment change (155650 -> 155888).

sdk-java workflow meta-test 117 passed / 5 skipped; eslint exit 0, prettier
clean; check-workflow-size.sh passes.

* fix(ci): calibrate the Java disk floors and fail closed in the meta-test

Review round on this PR found the gate could not fire in the two shapes it
exists for, and that the meta-test could go green through the same drift it
was added to prevent.

R1-2: every lane ran the shared 2 GiB / 100k-inode default. The #16003
incident was a used-ratio refusal — the broker reported 0.94 while the
952 GiB volume still had tens of GiB free — so no absolute floor can express
that threshold, and 2 GiB is below what these lanes write: a host with 4 GiB
free passed the gate and then died mid-suite. Give the five heavy lanes the
repo's build-sized 8 GiB floor, leave flyway-migrations and the shared value
alone, and add an opt-in DISK_FLOOR_MAX_USED_PCT arm to check-disk-floor.sh
for the two broker lanes (90%).

R2-1/R2-2: the corpus came from `runs-on` text and skipped jobs outside it,
so a lane routed through an expression or a job output shipped ungated while
the suite stayed green, and nothing asserted the hosted-only jobs carry no
gate. Fail closed: an explicit hosted-only allowlist, a gate required for
every other job, a total-count assertion, and a negative assertion on those
two jobs. Pin the per-lane floors so dropping one is a test failure.

Verified: sdk-java-workflow suite 117 passed, 5 skipped; it goes red when a
gate is deleted, when one is added to a hosted job, and when a floor is
removed. check-disk-floor suite 12 passed, including four new ratio cases.

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: qwen-code patrol <[email protected]>
2026-11-04 17:57:04 +08:00
易良andQwen-Coder b383efb854 fix(ci): widen the built-tree download window for eight shards (#16285)
* fix(ci): widen the built-tree download window for eight shards

Eight shards now pull the same unit-test-tree artifact at once, so the
download cap has to clear the blob's tail latency rather than the median.
On main the slow host class took 4m52s, and two consecutive runs expired
twice at the five-minute cap before the unpack ran, which failed the
required Linux check with no test involved.

Raise both attempts to fifteen minutes and pin the pair in the
build-to-shard handoff contract.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): size the shard download cap from the measured tail

Review flagged that the outgoing comment claimed a pool host had stalled
"for over a quarter of an hour", which the new fifteen-minute cap then
contradicts. That claim came in with the sharding commit and was never
tied to a run; neither incident can prove how far past five minutes the
tail went, so the comment now states only the measured distribution.

Also correct the number: the slowest completed shard took just under five
minutes, and it was a different shard that expired, on two consecutive
runs. Add the missing shard-side ceiling contract, mirroring the build
job's: every blocking step cap has to fire before the job cap, because a
cancelled job skips the failure-gated disk-pressure collector.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-04 17:41:55 +08:00
qqqys a85370b38f feat(cli): run the Ink TUI's model rounds on the shared loop's round (#16206)
* feat(cli): run the Ink TUI's model rounds on the shared loop's round

With experimental.unifiedAgentLoop on, every model round of the Ink TUI
goes through the shared loop's runModelRound, which collects the tool
calls, drops those of an attempt a retry or fallback replaced, and turns
the client's terminal events into a stop reason. The TUI keeps its
stream processor and its tool step. Adds the Ink parity suite.

Part of #15327.

* test(cli): tighten the Ink parity suite and fix the no-AK script pin

- add the suite to the no-AK script pin
- check each run's request count, and through the debug log that only the
  run with the setting on sends its rounds on the shared loop
- compare the system prompt, the tools offered and recorded usage
- run each scenario once, without the root config's retries, with a
  source pin
- describe the suite as it is in the design doc
- say in the setting's description that the Ink TUI sends its model
  requests through the shared loop
2026-11-04 07:36:48 +00:00
56fec7f3f5 ci: shard the Linux unit tests and web-shell smoke, and reuse cached builds (#16133)
* ci: shard the Linux unit-test lane eight ways behind one build

The Linux Test job ran 45-59 minutes: a ten-minute prepare build, then the CLI and core suites back to back. A new test_build job runs the prepare build once and uploads its outputs; eight shards install without the build, unpack them and run --shard=N/8 of every Vitest suite. Test (ubuntu-latest, Node 22.x) becomes an aggregate that passes only when the build and every shard pass.

* ci: record ci.yml's growth from the test_build prelude

test_build carries the same pre-checkout cleanup, checkout and install prelude as the test shards; ci-platform-lanes.test.js pins the two copies identical.

* ci: record the Linux lane's two new jobs in the PR-stage budget

test_build and the test_ubuntu aggregate add two jobs; the declared timeouts drop from 305 to 295 minutes.

* ci: shard the web-shell E2E smoke four ways

The smoke lane ran 44-46 minutes on every recent PR, about 33 of them in the browser tests at two workers, and is the longest lane once the Linux unit tests are sharded. Each shard runs --shard=N/4 of the smoke; the transcript gate runs on the first. web-shell E2E Smoke (ubuntu-latest, Node 22.x) becomes an aggregate over the shards.

* ci: bound the unit shards' tree download and retry it once

A pool host stalled 17 minutes in download-artifact while the other shards took under a minute.

* ci: rebuild only the workspaces a change reaches

scripts/ci-build-cache.mjs hashes each workspace's tracked build inputs (tests excluded) with its workspace dependencies' hashes and the shared root inputs. test_build and the web-shell smoke shards restore the newest cached build and rebuild only workspaces whose hash changed. A push to main always builds from scratch and saves, so a pull request starts from a clean build of main.

* docs(ci): describe the incremental build cache

* ci: keep the build cache path the same on every pool host

actions/cache versions an entry by its path; under runner.temp the path differs per host, so a run could never restore another host's save.

* ci: save the ARM64 build cache from the smoke lane, and run the aggregates on the shards' runners

The smoke shards run on the ARM64 pool, which test_build may not, so the first shard saves the clean build it made when its restore found nothing. The two aggregate checks queued 9 and 13 minutes for a hosted runner after their shards finished; they now take the shards' runners.

* ci: bound the sharded test step and skip shards when the build fails

Address the first automated review round on the Linux sharding lane:

- Gate every build-consuming shard step on `test_build` success. `!cancelled()`
  stays true when a needed job fails, so a failed build still started all eight
  shards, each paying a full install before dying at the missing artifact and
  reporting artifact plumbing as its failing step.
- Drop the test step cap to 45 minutes. The job cap is 60, so the old 110 could
  never fire; a step timeout keeps the diagnostic steps below it alive while a
  job timeout does not.
- Keep the built-tree artifact for a week so a late "re-run failed jobs" still
  finds it, and correct the trigger comment that still named three post-merge
  jobs when five now accept push.

Co-authored-by: Qwen-Coder <[email protected]>

* ci: keep disk telemetry for a failed build and correct the design doc

`test_build` installs with the disk sampler and then builds for up to its
whole timeout, but the sampler dies with the install step's trap and no
collector ran, so an ENOSPC build (or the OOM that took a shard host) kept
no evidence. Give it the same failure-gated dump and last-step upload as
`lint_and_static`, under its own artifact name.

The incremental-lint design still said the test job keeps normal prepare
behaviour; this lane now installs with `QWEN_SKIP_PREPARE=1` and unpacks
the tree built once by `test_build`. Correct both language versions.

Co-authored-by: Qwen-Coder <[email protected]>

* ci: share the built-tree definition and the build preflight, harden the guards

The archive step re-implemented `builtPaths()` from `scripts/ci-build-cache.mjs`,
so the tree the shards unpack had a second definition that could drift from the
one `pack` stores. A `paths` mode now prints that list for `tar -T -`, and a new
test pins the four artifact-name literals that tie the producer to the eight
consumers.

The cached branch of `build` also hand-reproduced a subset of `scripts/build.js`
and so skipped its ink-patch preflight: the same missing
`frame-controller.d.ts` self-healed on a cache miss and surfaced as ~50 tsc
errors inside ink's declarations on a hit. The preflight is now a function
reachable as `scripts/build.js --preflight` and runs before both branches.

The remaining review suggestions were all meta-test gaps: the standalone-runtime
expression and the `test_build`/`test` prelude were guarded by nothing that
compares them in order, `ci-build-cache.mjs`'s three CI modes were never
executed by a test, and the infra-classification filter's step name was not
joined to `ci.yml`. Each now has an assertion that goes red when the guard is
removed.

Co-authored-by: Qwen-Coder <[email protected]>

* ci: bound the build steps so the failure collectors can run

Round-2 review found four issues in the previous round's fixes and one gap
in the new build tests.

A job timeout cancels the job and skips the failure-gated collectors, so the
`test_build` steps that can block now carry caps below the job's 45 minutes:
the install, the build, the pack and the archive. The install caps are
identical across `test_build`, `test` and `lint_and_static` so the prelude
parity guard still compares the three copies field for field. A test pins
every blocking cap under the job ceiling.

The `--preflight` early exit was only asserted as a string, so it could be
renamed with nothing going red while the cache-hit path silently ran the
full build. A spawned case now copies `scripts/build.js` into the fixture
and asserts the flag exits without generate, a workspace build or the
schema step.

The CLI mode tests write extension-less `#!/bin/sh` stubs and join PATH with
':', which cmd.exe cannot resolve; the Windows lane collects that file and
downgrades its failure to a warning, so it would have carried a permanently
red block. The suite is excluded on win32 alongside the other bash-fixture
suites.

The `test_build`/`test` intersection grew to 16 with the disk collector, but
the floor stayed at 15, leaving exactly one shared step uncompared; the floor
now matches the membership. The cache-hit-with-changes branch — the code
that decides what is rebuilt and what is deleted — gains a case: a changed
workspace is purged and rebuilt by directory, and an untouched workspace
keeps what the cache restored.

ci.yml's recorded size baseline is updated to the current file so the next
PR gets the full growth allowance back.

Co-authored-by: Qwen-Coder <[email protected]>

* test(ci): keep cache fixture call logs outside the archived tree

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: 易良 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-04 14:52:12 +08:00
qwen-code-dev-bot 4d5e30e6f0 fix(test): wait for the RocketMQ port listener to clear after stop (#16252)
Aligns the parked-port release assertion with the script's own listener check: on a shared pool host a fresh bind probe can fail for a foreign socket or race the killed listener's asynchronous socket release (release run 41708821484, Scripts lane). The sibling case already uses waitForNoListener.

Refs #16248
2026-11-04 14:30:26 +08:00
e1bf0a2c24 fix(ci): resume linter archive downloads (#16067)
* fix(ci): resume linter archive downloads

Retry truncated actionlint and shellcheck downloads from the bytes already received instead of starting over on each attempt.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): resume only after the first linter download attempt

The retry loop passed --continue-at - on every attempt, including the first
one, right after the stale local copy was removed. A resume request against a
missing file appends only the tail, so the archive failed its SHA-256 pin and
the lane died. The PR's own test asserts the opposite: the first request must
not resume, retries must.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy7yxvq7f

* fix(ci): bound linter download retries to the CI step budget

Three review findings on the resume loop:

- Five attempts at --max-time 90 plus 5/10/15/20s of backoff is 500s,
  which outlasts the 5-minute 'GitHub CI helper checks' step that runs
  this installer (ci.yml:1433). On a stalled asset host the fourth
  attempt was cut off by the step, so neither the final exit nor the
  same step's --actionlint/--yamllint/helper tests ran. Three attempts
  put the worst case at 285s, in line with the ~273s bound of the
  curl --retry 2 call this loop replaced.

- curl resolves --continue-at - against a missing file to offset 0 and
  sends no Range header, so the old comment described a hazard that
  cannot happen and the first-attempt guard it justified was inert on
  the wire. The comment now states what the loop and the 33/36 branch
  actually do, and the guard is gone.

- Neither the 33/36 rm -f nor the attempt cap was covered: deleting the
  rm -f and raising the cap to 99 both left the suite green. Two tests
  now pin them, each verified to fail under its own single mutant.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvymz2cq81

* fix(ci): state the real resume budget and name the failed archive

Address the three review Suggestions on this PR's own new code:

- Budget comment: 285s is per archive, and the steps that provision both
  archives inside one 5-minute step can still cut the second one off.
  Also drop the PR body's "up to five attempts", which never matched the
  three the code ships.
- Terminal silence: the last attempt exits before the retry line, so a
  continue-on-error run ended on "retrying in 10s..." with no archive
  named and no exhausted budget. Echo that line, keeping curl's own
  exit status.
- Range-refusal rationale: measured curl exits 33 and leaves the
  partial byte-identical, so the rm -f is load-bearing because every
  attempt would otherwise re-send the same offset, not because a
  second body gets appended. The stub now models that sequence
  (28 with a partial, then 33 untouched, then a clean full body).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw0d4pwgah

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-04 10:53:11 +08:00
易良andQwen-Coder 4593a3b8f2 fix(ci): run desktop smoke headless on rpm roots without Xvfb (#16211)
* fix(ci): give the RHEL 10 desktop smoke a headless display

RHEL 10 removed xorg-x11-server-Xvfb from every repository: BaseOS,
AppStream, CRB, extras and EPEL 10 contain no such build, so the rpm
build-root dependency step failed one step after #16154 fixed the EPEL
bootstrap (`No match for argument: xorg-x11-server-Xvfb`, run 41642580745,
job 125940965630).

Keep Xvfb for the Fedora 44 leg and give the RHEL 10 smoke a real display
instead: a weston headless compositor with rootless Xwayland on top, since
GTK3 has no headless backend and WebKitGTK always needs a display server.
Export WAYLAND_DISPLAY before Xwayland is launched, wait for both sockets
with a hard timeout, and pin the app to X11 so the bridge is actually used.

Verified end to end on a Desktop Checks dispatch of this head (run
41652880267): the RHEL 10 rpm lane passes dependency installation and the
packaged smoke, and the Fedora 44 lane keeps its xvfb-run path. The three
workflow meta-tests that parse this file pass (161 passed, 1 skipped).

* fix(ci): key the rpm display arm on the matrix and harden its meta-test

Review round 2 findings on #16211:

- R2-1: the display arm keyed on an artifact_suffix literal while the EPEL
  bootstrap it depends on keys on matrix.epel. Declare matrix.display
  ('xvfb' on Fedora 44, 'weston' on RHEL 10) and branch on that, so a leg
  added later picks a server instead of inheriting an arm by accident.
- R2-2: the xvfb-run arm shipped a shell function through declare -f into a
  child bash without -e -u -o pipefail. Run the same npm command in both
  arms so both keep the step's error regime; assert no declare -f remains.
- R2-3 / R1-4: bound the shared package list by the conditional that follows
  it instead of a prose comment, ignore comments when checking it, and assert
  the xvfb/weston arms, the weston socket name against WAYLAND_DISPLAY, and
  the Xwayland display number against DISPLAY and the X11 socket.

* fix(ci): fail the rpm display arm closed and pin the smoke's wiring

R2-1: the install arm keyed on `matrix.display` but defaulted every unknown
value to the weston arm, which only works where the EPEL bootstrap above has
run. Turn it into an explicit `case` with an error arm, and assert from the
matrix that a non-Fedora root taking the weston arm has EPEL enabled, so a new
leg with `display: 'weston'` and `epel: 'false'` turns the meta-test red
instead of dying at `dnf install` in a release run.

R1-4: restore the two pins the previous rewrite dropped
(`weston --backend=headless-backend.so`, `XDG_RUNTIME_DIR`) and bind the rest
to the step's structure rather than to bare substrings: the weston readiness
wait must sit between the two launches, the helper must `return 1` on timeout,
and the EXIT trap must reap both daemons. Each added assertion was checked
against the mutation it names (all five turn red; the unmutated tree stays
green).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw1dkg4zbx

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-04 10:40:34 +08:00
易良 b5975157d1 fix(ci): drop the healthy-default apt gate and prefer the VPC intranet mirror (#16260)
* fix(ci): drop the healthy-default gate so a fast apt burst cannot veto the mirror

The merged probe (#16218) kept a third gate: override only when the default
measured below 250 kB/s. On the mainland hosts the deb.debian.org path is
bimodal — run 41743539611 on hz4-2 probed 372 kB/s (a fast 512 KiB burst), so
the gate kept the default, and the sustained 200 MB transfer then throttled to
45 kB/s and hit the 60-minute cap again.

Remove that gate. The two mirror-side gates already cover the cases it was
meant to protect: the 1 MB/s absolute floor rejects a "slow vs slower"
override, and the 4x factor keeps a genuinely fast default (the Hong Kong
hosts) on the built-in source. The hz4-2 sample (default 372 kB/s, mirror
1.97 MB/s) now selects the mirror; the Hong Kong and both-slow shapes are
unchanged. The contract test pins the floor, the factor, and the absence of
any healthy-default gate.

* fix(ci): prefer the VPC intranet apt mirror to bypass the capped public egress

The public mirrors still traverse the pool's capped 100 Mbps public egress that
throttles under concurrent load — the exact problem configure-maven-mirror.sh
solves for Central by preferring mirrors.cloud.aliyuncs.com, which answers from
inside the VPC and consumes no public bandwidth. Add it as the first apt probe
candidate. Off the VPC (hosted runners, Hong Kong hosts) it is unreachable, the
probe measures 0, and a reachable public mirror wins — so no host map is needed
and the probe self-verifies on each build.
2026-11-04 10:40:20 +08:00
易良 aaf523cfc4 fix(ci): probe the apt mirror so mainland E2E hosts can build the sandbox image (#16218)
* fix(ci): probe the apt mirror so mainland E2E hosts can build the sandbox image

The docker E2E leg builds the sandbox image per push under a SHA-scoped tag, so
on any host whose image store no longer holds a recent build, the two apt-get
layers are downloaded again. deb.debian.org serves the Hong Kong hosts at MB/s
and the mainland-China hosts at 11-20 kB/s, which turns that build into 2-3
hours against the job's 60-minute cap. Over the past week 97 of 127 docker legs
on the ecs-qwen-hz* hosts were cancelled this way, and the run reports
`cancelled` instead of a verdict.

Probe rather than pin a mirror or a host-name map: the build measures
deb.debian.org and two domestic mirrors, and passes --build-arg APT_MIRROR only
when the default is slower than 250 kB/s and a mirror beats it by 4x. The image
keeps deb.debian.org as its default source, so release builds, local sandbox
builds and the healthy Hong Kong hosts are unchanged, and no fleet inventory
needs to be maintained.

The probe's measurements are logged, so the next slow build can be diagnosed
from its own output.

Refs #16214

* fix(ci): require an absolutely fast mirror before overriding the apt source

The relative guard alone only asked a mirror to be 4x faster than the default,
so on a host where both endpoints were slow the mirror still "won" and moved
the host onto a source it never needed. Require 1 MB/s in absolute terms too:
below that the build is slow either way, and the extra cache key is not worth
it.

* test(ci): pin the apt-probe threshold values, not just the gate structure

The new contract test asserted the gate variables and comparisons but not
their values: mutating slow_default 10x up or fast_mirror 10x down left the
suite green while flipping the both-slow host shape onto a still-slow mirror
(review finding on #16218). Pin both literals with a word boundary — a plain
substring pin would still match the 10x-up mutation — and witness both
mutations reddening the suite before restoring the values.

* test(ci): require the mirror ARG in both Dockerfile stages

A single-match assertion survived deleting the runtime-stage ARG APT_MIRROR
(R1-1, mutation-witnessed by review): the builder declaration satisfied the
regex while the runtime apt layer silently kept the slow default. Count both
declarations instead; deleting either stage's ARG now reddens the suite.
2026-11-04 05:52:30 +08:00
a8e6c2ee57 ci: scope triage finalize concurrency to PR runs (#16070)
* ci: scope triage finalize concurrency to PR runs

Co-authored-by: Qwen-Coder <[email protected]>

* test(qwen-triage-finalize): compare the concurrency group with whitespace flattened

The group is a folded YAML scalar, so its rendered value keeps a newline
next to the more-indented continuation line. Pinning the literal string made
the assertion fail on the PR's own workflow file (Test (ubuntu-latest,
Node 22.x) at d190b876c4), while GitHub Actions treats whitespace inside
${{ }} as insignificant.

Compare the flattened expression instead, matching the normalizer already
used for multi-line conditions in capture-tmux-ci.test.js. The workflow file
does not change.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvyeef987p

* test(qwen-triage-finalize): pin the folded group without normalizing

The more-indented continuation made the folded scalar keep a literal
newline, which the whitespace normalizer hid, so the assertion no longer
pinned the committed value. Put the expression on one physical line so it
folds to the single-space form, assert the parsed value directly, and
correct the comment that credited the wrap to the formatter.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code patrol <[email protected]>
2026-11-03 20:45:00 +08:00
jinye b1cd4ac0c6 refactor(managed-agent): align shared MySQL schema conventions (#16143)
* docs(managed-agent): document database tables and columns

* refactor(managed-agent): align shared MySQL schema conventions
2026-11-03 12:00:29 +00:00
Shaojin WenandQwen-Coder 68c9fa27fb feat(verify-pr): add local Docker recipe with a host-side model proxy (#16077)
* feat(verify-pr): add local Docker recipe with a host-side model proxy

A local /verify-pr round needs the same isolation the CI lane provides,
but the skill only documented the boundary check, not how to run the
model credential through it. Add scripts/verify-pr-model-proxy.mjs, a
host-side relay that holds the real key and lets the container call the
model with a per-run bearer token, mirroring the CI lane's inline proxy
(POST /chat/completions only, nonce health check, per-chunk idle
watchdog). The listen address is a flag so the proxy can bind the Docker
bridge, and readiness is printed as one JSON line.

The SKILL recipe pairs it with a docker run template: the token travels
as QWEN_VERIFY_PROXY_TOKEN (not OPENAI_API_KEY, which the isolation gate
refuses), and the container's provider entry reads it via envKey. The CI
lane keeps its inline copy — the proxy holds the real key, so it must
not execute a script from the PR-head checkout.

Verified: 11 new pin tests plus all existing verify-pr suites (120
passed), and a real docker smoke — the gate passes inside
node:22-bookworm with the proxy token, refuses OPENAI_API_KEY with exit
4, and a container POST reaches a mock upstream with the real key
injected and the /v1 prefix preserved.

* fix(verify-pr): harden the model proxy after adversarial audit

Audit round 1 found the relay forwarded client hop-by-hop headers
(connection, transfer-encoding, expect, proxy-authorization, ...) to the
upstream, which RFC 7230 forbids and which fights undici's own framing
for the relayed stream. Strip them, with a chunked-body test that pins
the behaviour. Round 1 also found the CLI entry had no coverage: add
tests for the missing-config exit code and the printed JSON coordinates
(token override, base-path prefix, no real-key leak, health nonce), and
document that the printed baseUrl uses the listen address, so binding
0.0.0.0 yields a URL clients cannot dial.

Rounds 2 and 3 (open-ended re-read plus adversarial passes over the
updated diff) found nothing further. All verify-pr suites: 123 passed.

* fix(verify-pr): address review findings in the model proxy and checker

Round-1 review (12 inline findings at 70aed891bf). Proxy: abort the
upstream when the client disconnects before headers instead of paying
for a completion nobody reads (R1-1); make the idle watchdog pause-aware,
re-arm on drain, and match the client's own 240 s stream tolerance
(R1-3); bracket IPv6 listen hosts via an exported formatter and normalize
base paths once (R1-12). Checker: add --context so a host-held snapshot
pins the expected head instead of the writable artifact copy, and refuse
an unusable snapshot rather than silently falling back (R1-9).

Each guard is pinned by a test that was verified to go red when the guard
is removed (six mutations: pre-header abort, pause guard, per-chunk
re-arm, body error handler, IPv6 bracket, --context).

* docs(verify-pr): harden the local container recipe and record the design

Recipe: run the key-holding proxy and the checker from a trusted
checkout, never the PR tree (R1-6, R1-9); background the proxy with a pid
file and stop it at the end (R1-4); read the key instead of inlining it
(R1-5); use a named detached container driven by docker exec (R1-7);
require a standalone clone, the host uid and a fresh HOME (R1-8); mount
the full host-held metadata snapshot rather than the four-key artifact
copy (R1-10); and attest only inside, after rejecting credential-shaped
ignored files (R1-11). Add the bilingual design record (R1-2), cross-link
it from the local-round design, and pin the recipe's trust lines in tests
so reverting them turns the suite red.

* fix(verify-pr): address round-2 verification findings

Round 2 at 3509c2d1aa reported one Low and four Suggestions.

- Relay the exact <basePath>/chat/completions path. A suffix match also
  forwarded /admin/chat/completions to the upstream.
- Pin the response content-encoding strip with a gzip upstream: undici
  hands the proxy decoded bytes, so forwarding the header makes the client
  gunzip plain text.
- Reject credential-shaped ignored entries at any depth (a nested
  secrets/.env included) instead of only a root .env/.env.*.
- Name the round's engine as the released CLI and offer the tree build for
  a round that must exercise the PR's own CLI.

The CI inline copies keep their existing two-header strip: that lane is
untouched by design and a change there needs its own workflow review.

Tests: 141 passed. Three mutations verified red (exact path, gzip header,
nested credential pathspec). The review-pr check at this PR is red because
the bot lane exhausted ten retries on "429 rpm exceeded", not because of
this diff.

* fix(verify-pr): close round-3 trust-boundary findings

Round 3 at 3ccf38697d reported two Criticals against the recipe, one
fix-induced Critical, and four Suggestions.

- prepare, the credential scan, the isolation gate and the checker now all
  come from <trusted-clone>; prepare runs with the PR clone as its cwd so
  the HEAD check still applies (R2-1).
- The base commit's .qwen is extracted and mounted read-only over the
  tree, so the round's instructions and project settings cannot come from
  the PR (R2-2).
- The attestation is passed to the shell that starts the round, because
  every docker exec is a fresh shell (R1-11, fix-induced).
- The proxy token is exported from proxy.json and passed with a bare -e;
  the file is chmod 600 and removed at cleanup (R2-3).
- Connection-nominated hop-by-hop headers are stripped (R2-6).
- Recipe pins now enumerate every checker call site, require a non-empty
  docker-run slice, and seed the writable checker snapshot so a fallback
  regression is caught (R2-4, R2-5).
- New verify-pr-credential-scan.mjs lists ignored files one by one,
  refuses credential-shaped names at any depth, and leaves node_modules
  and dist alone — replacing the pathspec check that refused every
  installed or built tree (round-3 Low).

Tests: 148 passed; ten mutations verified red.

* fix(verify-pr): address round-4 review findings

Four P2 findings at 441a523927:

- The credential scan now limits git enumeration with credential pathspecs
  and depth-aware node_modules/dist excludes, so an installed tree's
  multi-megabyte ignored listing never overflows the output buffer.
- Those excludes apply inside workspace packages too, so packages/*/dist
  and packages/*/node_modules are not mistaken for project credentials.
- The recipe waits for proxy.json to carry a token and validates the
  exported value, so a truncated background write cannot reach Docker.
- The pre-header disconnect test waits until the mock upstream has
  recorded the request before destroying the downstream socket, instead
  of racing the proxy's own (correct) early abort.

Tests: 150 passed; three mutations verified red (enumeration pathspecs,
excludes, readiness loop).

* fix(verify-pr): refuse nested repositories in the credential scan

Round-4 verification at 19b14b1a8f left three Suggestions.

- A nested git repository is now refused instead of silently skipped: git
  does not descend into it, so a .env under tmp/pr-clone/ would pass. A
  vendored .git under node_modules/dist stays excluded like the files there.
- isExcluded is removed: the pathspecs already exclude those directories,
  and the surviving JS filter was unpinnable.
- The spaced Connection-nomination list is pinned; dropping trim made the
  proxy answer 502.
- The node_modules/dist credential exclusions are documented as deliberate.

Tests: 152 passed; two mutations verified red (nested refusal, trim).

* fix(verify-pr): address round-5 review findings

Four Criticals:
- The key entry works without a TTY: an interactive shell prompts, an
  agent's shell reads REVIEW_OPENAI_KEY_FILE and fails with a named error.
- prepare disables fsmonitor, hooks and credential helpers on the
  PR-controlled clone, and the scan refuses credential-bearing remote URLs.
- The proxy token is re-derived from proxy.json in the docker run step,
  because each step is its own shell.
- The credential scan covers .env~, *.p12, id_rsa*, gha-creds-*, and the
  other credential families the repo's own .gitignore names.

Suggestions:
- Guard call site moved to <trusted-clone>, <clone> defined for watch mode.
- The newest verify comment is snapshotted as previous-report.md.
- The real key is unset once the proxy owns it; the token is unset at cleanup.
- `{1..100}` replaces seq for macOS; --user and the proxy token's
  non-credential status are pinned; the scan abort is structural (`|| exit 1`).
- Step 6 wording matches the base-pin; the docs narrow the base-pin scope for
  root AGENTS.md/QWEN.md, list the real CI divergence, explain content-encoding
  in English, fix the zh token wording, and record the checker --context
  contract and the ownership attribution.
- The readiness gate compares the health nonce; --port is removed.

Test gaps closed: gzip header assertion, mid-body disconnect, reset event,
PUT, bare-base exact path, positive header passthrough, 502 transport
failure, full paused-stream equality, token shape, publish --context pin,
checker second positional, scan path fixtures, headers-only idle window.

Tests: 159 passed; twelve mutations verified red.

* fix(verify-pr): close round-5 verification findings

Two Lows reproduced at b5c717a5d5:
- prepare's hardening named clean filters but only disabled fsmonitor,
  hooks and credential helpers. It now enumerates every local
  filter.*.clean/smudge/process and empties it with a -c override before
  any git read; a planted-filter fixture proves the override with a
  positive control that fires without it.
- The nested-repository refusal over-refused installed trees whose
  node_modules holds a real vendored git repo (git reports the directory
  entry, which the exclude pathspec cannot match). The enumeration is now
  segment-filtered in JS, so vendored dependencies stay excluded while a
  nested project repository is still refused.

Suggestions and nits:
- The remote-URL guard reads url.*.insteadOf as well as remote.*.url and
  refuses any http(s) userinfo, including a token used as the username;
  ssh://git@ stays benign.
- Public keys (.pub) are no longer treated as credentials, and a
  backup.env~ fixture pins the trailing-tilde family.

Tests: 163 passed; six mutations verified red (filter overrides, vendored
segment filter, userinfo breadth, insteadOf enumeration, public-key
exclusion, backup.env~ pathspec).

* fix(verify-pr): close round-6 verification findings

Co-authored-by: Qwen-Coder <[email protected]>

* fix(verify-pr): close round-7 verification findings

Co-authored-by: Qwen-Coder <[email protected]>

* fix(verify-pr): address round-8 review findings

Co-authored-by: Qwen-Coder <[email protected]>

* fix(verify-pr): close round-9 review findings

Co-authored-by: Qwen-Coder <[email protected]>

* fix(verify-pr): close round-10 review findings

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 14:13:59 +08:00
276a447ef0 fix(ci): interleave PR review endpoint cohorts and report final routes (#16151)
* feat(ci): support same-model endpoint splits in PR review canary

The endpoint override derived the cohort by comparing the routed model
name against the configured canary/default models, so configuring the
SAME model on both cohorts — a pure quota split across two gateways —
labelled every PR 'default' and silently disabled the override.

Derive the cohort from the bucket predicate itself instead
(review_cohort_for_bucket: same clamp, same PR-number bucket test as the
model router), so model name equality no longer matters. The notice's
cohort field remains the attribution when both cohorts run one model.

* fix(ci): interleave review endpoint buckets and report final routes

Co-authored-by: Qwen-Coder <[email protected]>

* style(ci): apply Prettier to the review endpoint bucket test

The Lint & Static lane runs `node scripts/lint.js --prettier` on the
files a PR touches, and this PR's own test file was not formatted:
three `expect(...).toBe(...)` lines exceed the print width, so the
incremental lint step exited 1 on every head.

Formatting only - no assertion or fixture changed.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvzj4h0j9a

* fix(ci): share the endpoint host derivation and pin the cohort notice

- Extract openai_base_host() so the proxy bypass and the cohort notice
  derive the host from one place; the notice cannot name a host the
  traffic was not routed through.
- Drop the comments that still describe the removed model router.
- Pin the notice's bucket count (8% -> bucket 87/100), make the
  noKey/badUrl rows exact strings, and assert initial_openai_host so the
  three mutations that survived this suite now fail.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvznesli9g

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-03 02:20:46 +00:00
易良andQwen-Coder 5b7f9ec1c3 fix(ci): repair desktop RPM dependency and teardown checks (#16154)
* fix(ci): repair desktop rpm dependency and teardown checks

Co-authored-by: Qwen-Coder <[email protected]>

* test(desktop): use a counted space class in the EPEL checksum pin

`scripts/lint.js --eslint` runs on every file the PR touches, and the new
rpm dependency test spelled the two spaces in the pinned `echo` line as
literals: `no-regex-spaces` rejects them, so Lint & Static failed on the
incremental ESLint step.

`[0-9a-f]{64} {2}` matches the same two spaces the workflow emits; the
assertion still pins exactly what the release step writes.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvzj4h0j9a

* fix(desktop): keep the runtime group as a teardown target

Teardown collected daemon pids through the zombie-aware liveness predicate and
escalated SIGKILL only on leaders that were still alive, so a leader that had
already exited — reaped, replaced, or an unreaped zombie — silently dropped its
process group, which is teardown's only handle on it. The children the leader
spawned (MCP servers, tool subprocesses) then survived and kept writing under
the workspace about to be deleted, and the smoke reported a pass.

Collect every recorded pid, escalate on every collected group after the drain,
and keep the zombie-aware predicate on the "survived teardown" report so the
false positive this PR exists to fix stays fixed. The new fixture drives a
runtime whose leader exits while a writer in its group ignores SIGTERM.

Also pins the EPEL download URL and digest literals, drops the duplicate local
liveness predicate in favour of the shared helper, and covers the signal-probe
error arm (ESRCH gone; EPERM/EACCES alive, the rule the core helper documents).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvzpjyde9j

* fix(desktop): decide teardown on the runtime group, not its leader

The round-2 escalation signals the recorded pid's group (`kill(-pid)`), but
the verdict and the drain still asked `isProcessAlive(pid)`, which answers
"gone" as soon as the leader exits — the exact topology
`SMOKE_FIXTURE_EXIT_AFTER_SPAWN` reproduces. In that state `waitForExit`
returned on its first poll, so the 5 s drain was skipped and the
`survived teardown` branch was unreachable.

`isTeardownTargetAlive` probes the group first and falls back to the leader
where there is no group to signal (win32), so the drain and the leak verdict
cover the children the group can still hold.

Also drops the "mirrors the canonical … keep both in sync" claim from the
desktop probe's comment: it diverges from core's `isPidAlive` on the
pid <= 0 guard, the zombie exclusion on the EPERM/EACCES arm, and the
vanished-/proc answer. The comment now states those differences and why
teardown wants the dead-on-ENOENT direction.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvzrp44z9n

* fix(desktop): stop counting unreaped zombies as a live teardown group

`waitForExit` asks `isTeardownTargetAlive` whether a daemon's group still
exists. `kill(-pid, 0)` also answers for members that can no longer write, so
a daemon that exited and that the container's PID 1 has not reaped kept its
group "alive": the drain burned its five-second window, the second probe still
returned the pid, and teardown reported "the packaged runtime survived
teardown" for a runtime that was already gone.

On Linux the group probe now counts only members that can still write: scan
`/proc` for entries whose pgrp is the recorded pid, and report the group
stopped when every member is a zombie. A scan that observes no member keeps
the group probe's answer, so a hidden /proc cannot end a drain early, and the
group handle still wins over the leader handle.

Measured on a real zombie group (setsid + _exit, parent never reaps): /proc
state Z, `kill(-pgid, 0)` exit 0, `isProcessAlive` false, and
`isTeardownTargetAlive` true before this change, false after it.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvzy4lf49w

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 10:18:16 +08:00
jinyeandQwen-Coder 90eb13e4c6 refactor(managed-agent): Make runtime backends optional Maven modules (#16141)
* refactor(managed-agent): Split runtime backends into optional modules

Extract five backend implementations behind the shared provider SPI and support selected-backend executable distributions. Preserve SQL ownership, defer unavailable historical work, and make Spring own SSH connection cleanup.

Co-authored-by: Qwen-Coder <[email protected]>

* refactor(managed-agent): Narrow backend split to Maven modules

Keep optional backend dependencies and safe assembly while deferring selective standalone distributions. Restore default packaging and document the provider configuration.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 02:04:43 +00:00
易良andQwen-Coder 4dce337ea2 feat(ci): let the PR review canary cohort carry its own endpoint (#16130)
* feat(ci): let the PR review canary cohort carry its own endpoint

The review canary split (QWEN_PR_REVIEW_MODEL_CANARY/PERCENT) only moves
the model name; both cohorts share REVIEW_OPENAI_BASE_URL and its key, so
a rate-limited canary model also burns the shared gateway's quota — the
shape behind the 429-storm review timeouts.

Add an optional endpoint pair for the canary cohort:
QWEN_PR_REVIEW_BASE_URL_CANARY (variable) + REVIEW_OPENAI_API_KEY_CANARY
(secret). When the bucket routes to the canary and both are set, base URL
and key move with the model and the canary host joins NO_PROXY. A URL
without its own key, or an unparseable URL, degrades to the default
cohort with a warning — forwarding the default key to another provider
would leak it. A canary model identical to the default model disables the
override (buckets are indistinguishable). The cohort notice now prints
the resolved cohort so rollouts stay attributable.

Unset, behavior is byte-for-byte today's: model-only split on the shared
endpoint.

* fix(ci): drop inert canary NO_PROXY append and record the workflow's new size

The canary endpoint override appended its host to NO_PROXY, but nothing in this
job consumes it: configure_qwen_network unsets HTTPS_PROXY/https_proxy/
HTTP_PROXY/http_proxy before the override runs, qwen does not consult NO_PROXY
when building its proxy agent, and the gh/git wrappers that do get the proxy
back only talk to github.com. Removing the two exports also removes the comment
that claimed a proxy-bypass symmetry the step cannot produce.

Lint & Static was red in "Check workflow file size": 276955 bytes against a
recorded 272192 with a 4096 allowance. The growth is the feature itself (the
env pair, the override block, the resolved-cohort field), not prose that could
move into a sibling file, so the baseline moves with the file rather than the
rationale being cut to fit.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvz69ico8s

* refactor(ci): simplify canary endpoint validation

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: qwen-code patrol <[email protected]>
2026-11-02 23:22:43 +08:00
Shaojin Wen 14de8a035e feat(verify-pr): inherit --publish consent on same-session re-verification (#16112)
* feat(verify-pr): inherit --publish consent on same-session re-verification

A maintainer who ran `/verify-pr <n> --publish` and then typed the
"有更新了,重新验证" follow-up trigger had to confirm publishing again:
the skill inherited the previous round's coordinates but deliberately
excluded publish consent, so every re-verification of an already
published PR repeated a confirmation the maintainer had just given.

Let a follow-up round inherit publish consent from the most recent
round for the same PR in the same session. The round must announce the
inheritance in one line before posting, `--no-publish` on the trigger
opts out and breaks the chain, and the most-recent-round semantics keep
a quiet round from reviving an older consent. Cross-session rounds
still require an explicit request: the phrase alone posts nothing, and
a published comment found on the PR is evidence of a past round, not
consent for a new one. The CI lane and watch mode are unchanged.

Update the two pin tests that fixed the old per-invocation-only rule
and pin the inheritance, announcement, opt-out, and cross-session
boundaries.

* fix(verify-pr): carry inherited publish consent through follow-up chains

Review of the first round found the consent chain broke after a single
inheritance: R1 invoked with --publish, R2 a bare trigger that inherited
and published, R3 a bare trigger whose most recent round (R2) was not
itself invoked with --publish — so R3 fell back to asking again, the
exact friction this change exists to remove.

Model consent as a per-PR state of the session instead of keying it to
the latest invocation: a --publish invocation grants it, a fresh
/verify-pr round without --publish or a trigger carrying --no-publish
withdraws it, and a bare trigger keeps it unchanged, so consecutive
bare triggers publish under the original grant until a round withdraws
it. Watch rounds keep their own standing consent and never change this
state. Also close the two review suggestions: the Hard rules bullet now
recognizes a request inherited within the same session, and the
argument hint lists --no-publish so the opt-out is discoverable.

Pins cover the grant/withdraw/keep mechanics, the three-round chain,
the watch boundary, the Hard rules clause, and the updated hint.

* fix(verify-pr): grant session consent on any explicit publish request

Second review round found two gaps. The design docs for the assets
coordinates still stated that publishing consent is per invocation,
which reads as an invariant and could silently revert the feature;
both language versions now defer to the skill's session-consent rule
while keeping "coordinates are not consent". And the grant keyed on
the literal --publish flag, while the publish rule also accepts an
explicit request in words — a words-request round published but seeded
no consent, splitting the friction this PR removes by request form.
The grant now rides on the explicit request, flag or words alike, and
the withdrawal matches it.

Also close the deep verification's coverage survivors: the withdrawal
is pinned as one contiguous sentence (a fresh round keeping consent
now fails), the chain's upper bound, the announcement's naming duty,
and the evidence-of-a-past-round clause each have pins, and an
inherited publish now carries its one-line announcement into the
comment body immediately before the attribution footer, so a PR
reader can tell it from a per-round request.
2026-11-02 22:22:00 +08:00
jinyeandQwen-Coder f643e579c5 fix(managed-agent): Restore profile-owned runtime verification (#16134)
Keep the shared Failsafe configuration in pluginManagement so ordinary
release verification does not activate environment-specific integration tests.
Preserve the dedicated profiles and executable artifact assertions.

Backport the same POM repair to the pinned 0.26.5 source with checked patch
digests, and verify both patches before applying either one.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 13:50:11 +00:00
38b71299c8 feat(desktop): build and publish rpm installers (#16060)
* feat(desktop): build and publish rpm installers

Add Fedora 44 and RHEL 10 release roots, strict RPM smoke checks, and artifact naming that keeps each root distinct.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(desktop): declare rpm runtime requirements and clear the yaml gate

The rpm legs built artifacts with no `Requires:` entries: Tauri's rpm bundler
writes them only from `bundle.linux.rpm.depends`, which nothing set, so
`verifyRpmDependencies` had no webkit soname and no glibc floor to find and the
release could not pass its own gate.

- Add `rpm-fedora44.conf.json` / `rpm-rhel10.conf.json` overlays declaring the
  webkit soname and each root's glibc floor (2.43 vs 2.39 cannot share one
  config), and pass the per-leg overlay to `tauri build --config`.
- Quote-free `container: ${{ matrix.container || null }}` stays as is with an
  inline yamllint disable: quoting the expression would turn a null container
  into an empty string. The shape test pinning that literal moves with it.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy1jgjo76

* ci(desktop): record the rpm legs in the workflow size baseline

`desktop-release.yml` is now 49858 bytes, past its recorded 45089 plus the
4096 allowance, so `Lint & Static` fails on the ratchet before any linter runs.
The growth is the two rpm legs: their matrix entries, the per-leg overlay
wiring, and the roots' comments. Moving that prose out would not pay for it —
the legs themselves are the bytes — so the number moves with the file in the
same PR, as the gate asks.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy1jgjo76

* style(desktop): apply prettier to the rpm legs and realign the workflow shape pin

Prettier flagged .github/workflows/desktop-release.yml and
packages/desktop/scripts/smoke-linux-installers.js on head 10acaadeba.
test-release.js pinned that container line with two spaces before the inline
yamllint disable comment, which prettier rewrites to one; .yamllint.yml has
min-spaces-from-content: 1, so the pin now follows the formatter instead of
blocking it.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvy3ombr79

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-02 20:50:05 +08:00
jinyeandQwen-Coder d9d6fc4a4b refactor(managed-agent): split Agent and Session stores (#16090)
* test(managed-agent): audit lock order across store collaborators

Record the bilingual Store decomposition contract and entry ownership,
transaction, lock and caller inventories. Follow final collaborator fields
across source files while preserving owner-bound facade populations and
existing ambiguity, transaction and compiled-member checks.

Co-authored-by: Qwen-Coder <[email protected]>

* refactor(managed-agent): split Agent and Session stores

Move Agent and Journal responsibilities into immutable internal collaborators while retaining transaction facades and compatibility bridges. Resolve Journal dependencies at construction and migrate factories, fixtures, lock auditing and the bilingual implementation contract.

Record focused verification and outstanding full-gate requirements separately.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): repair store decomposition verification

Keep declared field receivers visible across method-local type scopes while retaining fail-closed nested class handling. Preserve the Hosted CSI dependency identity assertion at its new owner.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 12:46:43 +00:00
qqqysandShaojin Wen 9c96f5aa82 feat(mods): run prompt.submit hooks in headless runs (#15905)
* feat(mods): run prompt.submit hooks in headless runs

Mods' prompt.submit hooks can rewrite, add context to, or drop each headless prompt before UserPromptSubmit, @ processing and the transcript see it. Terminals keep skipping these hooks until they raise the event. Includes the design document for the four steps.

* test(mods): update bundled worker load response expectation

* fix(mods): preserve downstream prompt submit outcomes

* fix(mods): isolate prompt fallback from host continuations

* fix(mods): preserve latest local prompt outcome in recovery

* fix(mods): hear only a message's text and take any answer after next

A stream-json message with an image block reached the prompt.submit
hooks as its serialized blocks, so they were skipped as too long or saw
JSON noise, and a rewrite carried that JSON into humanInput and
turn.start. The hooks now hear its text blocks; a rewrite replaces them
and the other blocks follow it.

A hook's answer after its next changes nothing, but it was still
validated, so a hook that only watches and returns nothing, or spreads
the event back, failed and logged a skip on every prompt. Only a drop
there is now checked.

* fix(mods): keep prompt.submit to typed text and drop empty rewrites

- Show only the rewritten text in the transcript and UserPromptSubmit,
  not the serialized non-text blocks of a stream-json message.
- Skip prompt.submit for SDK messages that answer a tool call or carry
  tool blocks, and for messages without text.
- Drop a prompt the hooks rewrote to nothing instead of sending the
  model an empty prompt.

* fix(mods): send each prompt context once on its own

The Mods' prompt.submit context and UserPromptSubmit's additionalContext
share one hidden block, and the block was only skipped when the
conversation already held it whole. A Mod whose context changes every
turn made every block new, so an unchanged UserPromptSubmit context went
to the model again each turn.

The injected part now records the contexts it joins, and a context counts
as delivered when an earlier block holds it, so only the contexts the
conversation lacks are sent. A block without that record, as after
resume, still counts as one context.

* fix(mods): keep a message's blocks in the transcript after a rewrite

A stream-json message with blocks other than text is recorded with those
blocks serialized after its text. A prompt.submit rewrite recorded the
rewritten text alone, so the same kind of message showed two shapes in the
transcript. The rewritten prompt is now recorded with the blocks after it.

Adds the headless E2E test plan and its results.

---------

Co-authored-by: Shaojin Wen <[email protected]>
2026-11-02 09:14:14 +00:00
jinyeandQwen-Coder 85943db2b3 fix(ci): repair Javadoc for the 0.26.5 managed runtime release (#16114)
Apply the already merged documentation-only repair to the original release source before the unchanged full verification and missing-only publication gates.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 17:12:10 +08:00
易良andQwen-Coder 292c0da525 test(install): pin Windows PATH persistence contract (#16072)
* test(install): pin Windows PATH persistence contract

Co-authored-by: Qwen-Coder <[email protected]>

* test(install): format the pinned PATH assertion

Prettier collapses the single-string expect(...).toContain(...) call at 76
columns, which the incremental lint gate rejected on the previous head.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvygjkzk7s

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-02 15:44:05 +08:00
Shaojin Wen 0a7f260704 fix(managed-agent): reach the listeners the server resolves in upgrade start and verify (#16051)
* fix(managed-agent): reach the listeners the server resolves in upgrade start and verify

start and verify derived the control-plane URL from server.env's
QWEN_MANAGED_AGENT_SERVER_ADDRESS and SERVER_PORT, and the Harness URL from
its QWEN_MANAGED_AGENT_HARNESS_BASE_URL. A port or address set anywhere else
(config/application.yml, an argument, SPRING_APPLICATION_JSON, a relaxed
SERVER_ADDRESS) or a management.server.port of its own made start wait out
--timeout on a port nothing served.

ResolvedConfiguration now also prints serverUrl, healthUrl and
harnessBaseUrl as Spring binds them: server.port, server.address and
server.ssl, with a wildcard reached on loopback, and the health endpoint by
Spring Boot's management-port rules. start resolves them before it changes
any unit, verify before its rows. --server-url still names the control
plane; a port of 0 or below refuses until it does; a configuration that
does not resolve falls back to the server.env URLs with a notice.

* fix(managed-agent): keep the health name for a blank path mapping

A blank management.endpoints.web.path-mapping.health, such as an unset
${HEALTH_PATH:}, is ignored by Spring Boot's MappingWebEndpointPathMapper,
so the endpoint stays at /actuator/health. The resolver turned it into an
empty segment and sent start and verify to /actuator, where start waited
out its timeout on a healthy server. Fall back to the name when the
mapping is blank, as Spring does.

Also from review:
- the fallback notice says only the Harness URL comes from server.env
  when --server-url names the control plane;
- start's success line names the API URL next to the health endpoint;
- verify with --server-url resolves the configuration only for --tenant,
  as it did before, since it does not reach the Harness.

* fix(managed-agent): refuse listener URLs fetch cannot dial in upgrade start and verify

A link-local server or management address keeps its IPv6 zone in the
resolved URL (http://[fe80:0:0:0:0:0:0:1%1]:8080), and the WHATWG URL
parser behind fetch rejects every zone spelling. The health probe caught
the TypeError as "not UP", so start waited out --timeout on a URL it could
never send. The same held for an out-of-range port, a server.env address
with a zone on the fallback path, and a --server-url that does not parse.

listeners() now checks both URLs with URL.canParse after the no-fixed-port
refusal, before any unit changes, and names the zone when that is why. The
resolver test pins that the zone is kept, so the script can say so. The
verify test lists the health probe with the session reads instead of
slicing it off.
2026-11-02 12:57:52 +08:00
Shaojin Wen d36fdbdfe5 test(managed-agent): gate detach while a tool result is unknown (#16098)
The MCP detach must refuse before it releases anything when an
operation is recovery_blocked/outcome_unknown. Start an ext://hold
read, kill the stdio fixture, and call the Harness detach route; the
case requires the documented refusal, no release control, and the
operation still unknown.

The Hosted fork moves by the case's measured 27.4 s, booked at 55 s.
2026-11-02 11:44:00 +08:00
Shaojin Wen 7b4b353750 test(managed-agent): gate the two-server first release refusal (#16089)
Extend HostedMcpRestartFaultGatesIT with the FG12d cell #15750 still
carried: a Session configured with two MCP servers, the first release
refused before forwarding, and the same-id retry releasing both.

The case adds fixture2 only for its own boot, waits for both
configurations to settle, and requires three release controls: 503,
the same operation id answered 200, then the other configuration's
release answered 200. Both fixture processes must exit.

The Hosted fork moves by the case's measured 16.2 s, booked at 32 s.
2026-11-02 02:46:13 +08:00
Shaojin Wenandverify 4a5c487681 test(managed-agent): gate the expired-cursor resync answer (#16076)
* test(managed-agent): gate the expired-cursor resync answer

FG9e now advances the Session stream's replay floor past a lagging
client and requires one `agent.session.resync_required` frame on both
the public and WebShell streams, with no event id and the floor and
snapshot cursors it must reload through. The same cursor on the plain
events route answers `409 cursor_expired`, and the floor itself stays
readable.

This closes the expired-cursor cell the FG8 tracker (#14163) deferred
and #15750 carried. The case measures 21.3 s locally; the Hosted fork
budget and Verify step move with it.

* test(managed-agent): wait for projection coverage before advancing the floor

Awaitility treats a lambda that returns without failing as satisfied,
so the coverage guard must assert rather than return. Without this a
poll that lands before the lead's Turn is projected would skip the
advance and dereference a null window.

* docs(managed-agent): correct the resync case's tracker references

FG9e now carries the third case, so the relay-hold sentence must name
only the two resume cases. The FG8 record no longer predicts a slice-2
landing and points at the FG9e delivery instead.

* ci(managed-agent): record the resync case's measured range

The 60 s fork-budget entry was written from one 21.3 s run; the final
audit runs measured 21–23 s, so state the range the budget covers.

* docs(managed-agent): fix the stale step-ceiling header and pin note

The Hosted job's opening parenthetical still summed 291 while the tail,
the Verify step and the job ceiling had moved to 292/202/325. Also say
the replay-floor flag is a default pin, not something currently off.

---------

Co-authored-by: verify <verify@local>
2026-11-02 00:28:04 +08:00
易良andyiliang114 7f774219ae test(opentui): run the Windows preview installer suite through cmd.exe (#16028)
* test(opentui): launch the Windows preview entry through cmd.exe

The PowerShell preview installer preservation suite failed on every
nightly Windows run: the seeded qwen.cmd launcher printed nothing, and
the leftover batch child kept the file locked so teardown hit EBUSY.

PowerShell was only a way to run a .cmd from Node. Its `&` call plus
`exit $LASTEXITCODE` returned before the batch child wrote anything, so
run the entry through cmd.exe instead (`shell: true`).

PowerShell also writes its own startup cache under
%LOCALAPPDATA%\Microsoft\Windows\PowerShell. The fixture pointed
USERPROFILE at the home tree the assertions treat as installer-owned, so
every successful test failed on that cache. Point APPDATA and
LOCALAPPDATA outside the compared home.

* test(opentui): compare home files, not shell-created folders

With the launcher fixed, every case in the suite now reaches the
preservation assertion and fails on Windows because PowerShell creates
its own empty %USERPROFILE%\AppData folder shells on first run. Those
are the shell's account directories, not installer output.

Redirect APPDATA/LOCALAPPDATA outside the fixture home (so the startup
cache file stays out too) and compare only the home's file contents.

---------

Co-authored-by: yiliang114 <[email protected]>
2026-11-01 10:53:33 +00:00
Shaojin Wen 0f91cdedae fix(verify-pr): count suite runs and skip notices on closed PRs (#16050)
Follow-ups deferred on #16013:

- A round whose only checks were existing suites had a total of 0
  once suites moved to gates, and the checker refuses that. The skill
  now counts each suite run as one assertion, as it already did for a
  mutant run.
- Gates that record no results (an empty list, or entries counting
  nothing) no longer silence the size warning. When no suite ran, the
  round says in the report what its count holds.
- The blank-suite rule is JSON whitespace on both sides. jq's \S and
  JS's trim() disagreed on U+0085 and U+FEFF, so the checker accepted
  a list the publisher dropped.

Also, a round can queue for hours: the publisher no longer posts a
notice on a PR that closed meanwhile, unless it is finalizing its own
live status. Four such notices landed on #16013 after it merged.
2026-11-01 07:55:09 +00:00
93231beca5 test(managed-agent): assert A3 sibling call binding (#15995)
Co-authored-by: wang1408 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-01 06:57:12 +00:00
Shaojin WenandQwen-Coder 06c1f98971 fix(verify-pr): count test suites apart from the round's assertions (#16013)
* fix(verify-pr): count test suites apart from the round's assertions

Rounds folded whole workspace suites into assertions.json, so the
published "Scripted assertions" line measured the repo, not the round:
37 of the 252 counted reports in the 30-day census exceeded 1000, and
one round's 7737 held a few dozen checks of its own, the rest being
five suites counted again for every mutant run.

assertions.json gains gates: [{ suite, pass, fail, skipped? }], one
entry per existing test suite run as a targeted gate. The skill says
suite results and mutant runs stay out of pass/fail/total, and that a
gate red the PR causes still reaches the verdict as an assertion. The
checker refuses a malformed list and warns when over 1000 assertions
come with no gates. The publisher prints the gate totals on a line of
their own, numbers only, and does not let them decide the verdict.

Also, deferred from #15938: the paragraph for a merge-ready round with
failures now states the checker's rule instead of inviting the reader
to excuse the failures, and the artifact-check guard's empty-value legs
are pinned.

* fix(verify-pr): warn on empty gates and mirror the checker's shape rules

Address the three stage-2 triage suggestions: an empty gates list no
longer silences the suite-sized warning; the publisher's jq refuses
blank suite names and non-count skipped values (false/null) instead of
defaulting them, so a shape the checker refuses never renders totals;
and the design docs say 'all three' / '三者' where the rule count grew
from two to three.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(verify-pr): give the gates shape rule one witness and honest totals

Address the five R1 review suggestions:
- one shared fixture table drives both the publisher's jq and
  checkArtifacts, and both bound counts at 2^53-1, closing the 1e309
  divergence where jq clamped an overflow the checker refuses
- the skipped clause prints only when some entry records the field,
  so an unrecorded count is no longer published as a measured zero
- a trusted verdict over gate reds carries one advisory sentence in
  both languages pointing at the assertion ledger; still verdict-inert
- pin the skill's compensation sentence and name the gates-size
  warning in the local clear-list and the CI-path sentence
- bump the workflow size baseline to 375110: the parity comment, the
  conditional clause and the advisory exceed the remaining allowance

Co-authored-by: Qwen-Coder <[email protected]>

* fix(verify-pr): print the skip clause only for fully recorded gates

Address the two R2 review suggestions:
- the clause now requires every entry to record skipped; a mixed list
  published a partial sum as the run's skip total
- pin the advisory's trusted-verdict guard from the false side: an
  untrusted run with gate reds prints the totals but no advisory
- fixtures: a mixed list drops the clause, a recorded skipped:0 keeps
  it, and a timeout headline over gate reds stays advisory-free

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-01 02:26:31 +08:00
Shaojin Wen 143a8b0653 ci(sdk-java): run the Hosted admission gates even when the Hosted verify fails (#16010)
* ci(sdk-java): run the Hosted admission gates even when the Hosted verify fails

The admission step had no condition, so it ran only when every earlier
step passed. The Hosted verify fails most nights, and the step was skipped
on all 13 nights from 10-14 to 10-30 that the verify failed, including the
three since the Runtime Broker fault gates stopped being skipped the same
way. It ran on the other three nights and passed each time, in about two
minutes.

The step rebuilds the module with `clean verify` and needs only the CLI
bundle, the installed SDK and the MySQL service. Each gate class works
under its own random tenant, and on green nights it already runs against
the database the Hosted verify used. So it takes the condition the fault
gates and the O4 step use: run unless the run was cancelled, as long as
the SDK install succeeded.

The class check after it reads only the admission step's reports. It now
follows that step's outcome instead of the job status; otherwise it would
still skip after a red Hosted verify while the step it audits ran.

The pin for the fault-gate condition now covers both steps, and a new case
pins the class check's condition and position. The setup-step assertion
now names the offending steps in its message: vitest abbreviates the
array once a step name is long enough, and the install step's own name
already is.

* ci(sdk-java): keep the O4 gates reachable on red-verify nights

The admission step now runs on red-verify nights too, so the job's worst
case sums all four capped steps (291 min) plus the uncapped setup. Raise
the hosted-harness-mysql ceiling from 304 to 324 so a slow setup (the CLI
install retries three times) cannot cancel the job before the O4 gates,
whose !cancelled() condition would record them skipped.

Close the two pin gaps the review found: the red-verify condition test
gains the O4 row (all three conditioned steps are now pinned), and the
admission class-check test pins that the check step carries no
continue-on-error, so a dropped gate class still fails the job.

All three pins are mutation-witnessed at this head (ceiling back to 304,
O4 if: deleted, continue-on-error on the check step each redden exactly
their case); the nine sdk-java.yml parsing suites pass 709/709.

* ci(sdk-java): boot the admission gates on their own database and bump the size baseline

Now that the admission step runs after a red Hosted verify, it must not
share that verify's database. Managed schema validation refuses any table
outside the managed set, and four Hosted ITs create fg*_fire tables they
drop only during cleanup (the relay classes only while their Spring context
is active), while the isolation extension truncates only ma_ tables. A
class killed before its cleanup would leave one behind and fail both
admission gates at boot. On a MySQL 8.4.6 rig, an fg9b_fire table in
hosted_harness_test made the base command fail both classes with "Managed
schema must contain exactly the 38 target tables", while this command
passed both on a fresh qwen_admission_ci. The step now creates that
database the way O4 creates qwen_o4_ci.

The pin test adds the O4 row, pins that the class check has no
continue-on-error, custom shell or altered command, that no gate command
swallows its failure, and that admission boots on its own database. Both
steps' names are checked before use.

sdk-java.yml outgrew the ratchet: main's copy was already 3971 bytes over
its recorded 144802 without a bump, and this change adds the rest. Even
without its comments the file would exceed the ceiling, so the baseline
moves to the file's size.

* test(ci): pin the admission step whole and keep the install ahead of the verify

The admission step's command, env and shell are now pinned with literal
values. The hosted-gates class check counts a failed class as one that
ran, so an `||` fallback, a pipe, or -fn in the command or in MAVEN_ARGS
would have turned a red admission gate green, and a second -Dmysql.url
would have pointed it back at the shared database. A list of forbidden
substrings caught some of these and rejected harmless commands, so the
fault-gate and O4 rows go back to pinning only their condition and
continue-on-error; their commands are unchanged by this PR.

The three gate rows also pin that the SDK install comes before the Hosted
verify. After it, a red verify would skip the install and every gate with
it, on exactly the nights the condition is for.

Comments: the job comment opened with stale ceilings (162 + 20 + 50 + 20
= 252); it now gives the measured setup time (under 10 minutes on every
nightly from 10-14 to 10-30), states that what changed is the timed-out
Verify path, which used to skip admission, and says the CLI install makes
up to three attempts rather than retrying three times. The O4 row comment
says O4 has carried its condition since it was added.

* test(ci): derive the red-verify contract from the job and pin the setup allowance

The red-verify condition was asserted over three hand-listed step names,
so a gate added to the job later sat outside every assertion and a red
verify would skip it silently, as it skipped the admission gates. A new
case derives the contract from the steps after the Hosted verify:

- Only the two report checks that need the verify's output may run on a
  green job alone. Every other step's condition must be `always()`, or
  `${{ }}` around `always()`, `!cancelled()` or `failure()`, optionally
  narrowed by one step's outcome. Any other condition either has no
  status function, so GitHub prefixes `success() &&`, or still reads the
  job status. A narrowing step must itself run regardless of the verify.
- No step after the verify carries continue-on-error.
- Every step that invokes Maven, matched as a word so folded commands,
  wrappers and quoted paths count, carries the exact red-verify condition
  and a ceiling, so it enters the summed step budget.

The job-budget rows now carry the setup allowance (33 for the MySQL job,
15 for failover) and assert ceiling - steps against it, so raising a step
ceiling re-derives the allowance instead of quietly spending it.
2026-11-01 02:25:44 +08:00
Shaojin Wen 7bc00a3a08 feat(review): re-anchor on drift, run actionlint, account blockers, refuse retries (#16001)
* feat(review): re-anchor on drift, run actionlint, account blockers, refuse retries

Four fixes from the 2026-10-31 audit of seven review run ledgers
(#15057, #15377, #15385, #15578, #15853, #15955, #15974):

- Head drift posts the completed review at the new head (the re-anchor
  protocol) instead of abandoning the round — the maintainer's ruling
  on #15578, never landed in the skill until now. The once-per-review
  bound and the COMMENT cap survive; the marker withholds its anchor.
- script-lint runs actionlint's workflow-level checks (syntax, context
  availability, action refs) and gates them as [lint] Criticals; only
  the embedded run: shell stays deferred. #15974's unloadable workflow
  (runner context in jobs.env) is the regression test, measured against
  actionlint 1.7.12.
- pr-context writes a blocker inventory; compose-review cross-checks it
  against the round's blockerRulings and caps at COMMENT when the walk
  skipped a blocker (#15974's missed review-level blocker).
- submit documents --dry-run as the only sanctioned probe and refuses a
  re-post at the commit already landed (already-posted-this-commit) —
  the retry that replied this round's own roots back to themselves
  (#15853). The receipt records lastCommitId.

Each fix carries its rule text, pin tests, and a DESIGN.md incident.

* fix(review): isolate actionlint config and refresh lint on re-anchor

Both Criticals from the #16001 review, reproduced against the exact PR
source and closed structurally:

- The newly active actionlint invocation auto-loaded the reviewed repo's
  .github/actionlint.yaml, so a PR-added ignore-all rule suppressed every
  workflow diagnostic — including the unloadable-workflow expression this
  linter was activated to catch. It now runs against a private mkdtemp
  neutral config via -config-file (measured to override the discovery),
  fails closed when no private config can be created, and carries exactly
  the two trusted compatibility exceptions the repo's own lint script
  runs (self-hosted runner labels, environment.deployment schema lag).
  The regression plants the ignore-all config in a real temporary git
  repository and proves the expression finding survives.

- The re-anchor protocol dropped fixed authored findings but left the
  lint gate regenerating its findings from the old-head report, so a
  lint blocker the delta fixed was republished against the new commit.
  The protocol gains a refresh step (scratch worktree at the new head,
  script-lint --diff rewriting the report bound to the new diff) and a
  lintDiffPath compose field that makes the gate — body and ledger
  marker alike — hash the new diff as its freshness basis. Executable
  regressions pin both directions: fixed blocker gone (COMMENT, absent
  from body and marker), still-standing blocker keeps gating
  (REQUEST_CHANGES). A malformed lintDiffPath fails closed.

* fix(review): base-tree actionlint config, empty-diff parse, lane provisioning

Three findings from the round-3 review of #16001, each reproduced
against the exact source and closed:

- The private {} config blocked PR-planted suppressions but also dropped
  the repo's maintainer-merged compatibility exceptions (ECS runner
  labels, concurrency.queue, the $/ self-repository action reference),
  turning correct release-workflow changes into bogus [lint] Criticals.
  The trust boundary is now the MERGE-BASE tree's .github/actionlint.yaml
  (hex-shape-guarded git show, copied into the private file, {} only when
  the base has none); the head copy is never read, and the two trusted
  -ignore constants ride on top exactly as scripts/lint.js runs them.
  Pinned with a two-commit repo and a real-binary run showing the base
  exception honored while the planted ignore-all stays blocked.

- An empty refresh diff (the new head reverted the PR's last change) was
  treated as a parse failure: the range selection fell back to the old
  plan's context-inclusive hunks and promoted a pre-existing finding into
  a blocker the gate accepted as fresh (the empty diff has a valid hash).
  diffParsed now separates parse success from range count; empty and
  deletions-only diffs classify everything pre-existing. The reviewer's
  exact scenario is an executable regression.

- The lanes that run the gate provisioned no linters, so the new
  uninstalled-capping fired on CI and the real-binary regressions were
  skipIf'd out of every run. The review job and the unit-test lane now
  install the pinned, SHA-256-verified actionlint/shellcheck through the
  repo's own installer and promote the job-scoped dirs onto GITHUB_PATH
  (unconditional, advisory, Windows excluded). The 335 workflow meta-test
  pins pass unchanged.

* fix(review): lint in the worktree cwd, guard lint.js entry, pin the sha fence

Three gaps from the scripted deep verification of #16001 (50/55
assertions, 17 mutants), two of them in the round-3 fixes themselves:

- F1: actionlint resolves the config's paths: entries against its cwd,
  and the runner spawned it in the session checkout, so every
  path-scoped base exception was inert while the global keys kept
  working — the split that hid behind the labels-only test. The runner
  now receives the worktree root and spawns inside it with PWD aligned.
  The real-binary suite gains the path-scoped fixture it was missing: a
  benign concurrency.queue edit that must lint clean, and an attack cell
  where the base exception suppresses the schema-lag finding while the
  planted head ignore-all still fails to hide the expression.

- F2: the provisioning steps imported scripts/lint.js, whose
  unconditional top-level main() ran a full lint pass on import and
  never reached the install loop, silently under continue-on-error.
  lint.js now runs its CLI only when it is the entry point
  (import.meta.url guard, argv[1]-safe for node -e imports), and both
  steps assert each installer produced its binary before promoting the
  dir, so a future no-op install fails visibly instead of degrading to
  skipped tests.

- M15: the option-injection fence was unobservable — git's own
  rejection of a malformed option matches the guard's refusal, so the
  guard's mutation survived. A spawn spy now pins the fence directly:
  non-hex merge base never spawns git; a hex-shaped nonexistent one
  does (positive control with the exact argv).

Also acknowledges the verification's correction: the bot review's
$/prepare-release-runner example does not exist at this head (the base
config entry is precautionary); the queue exception at line 485 is the
one reachable path-scoped case, and F1 is pinned on exactly that shape.
2026-10-31 13:43:34 +00:00
edde639992 feat(cli): support exact-version updates without npm discovery (#11486)
* feat(cli): support exact-version updates without npm discovery

* fix(cli): repair build and close exact-version update review gaps (#11486)

The merge into this branch left two copies of the semver default import
in standalone-update.ts, so packages/cli no longer compiled (TS2300) and
every downstream gate went red. Drop the added duplicate, keeping main's
import position.

Also address the remaining review findings on the exact-version update:

- Verify the extracted archive's manifest.json version matches the
  requested version before activation, alongside the existing executable
  smoke test. The manifest is the field the installation reads back and
  the prepared-update gate compares against; an inflated manifest version
  would otherwise silently freeze all future prepared updates.
- Pin the yargs builder contract: --target-version must reach the handler
  as argv.targetVersion under the production .strict() parser, going red
  if the builder is dropped or the option renamed.
- Add the issue #11484 acceptance-criterion-7 E2E test: update a
  disposable standalone install to an exact version served from a local
  HTTPS release root while the npm registry sink records zero requests,
  asserting the installed manifest version and rollback retention.
- Document that an explicit target is not persisted: automatic updates
  move a standalone install forward again next session, and keeping an
  exact older version requires general.enableAutoUpdate: false.
- Add the Chinese version of the design doc with reciprocal language
  links.

* fix(cli): close standalone update review gaps from round 2 (#11486)

Address the second-round review findings on the exact-version update:

- Validate the extracted archive's full manifest identity (name, target,
  version) before activation instead of version alone. A mirror artifact
  with the right version but no name passed every check and installed,
  after which the directory silently stopped classifying as a managed
  standalone install; the gate now rejects it and preserves the install.
- Guard both version comparisons with normalizeVersion's own predicates
  (SEMVER_RE and semver.valid), so a regex-valid but semver-invalid
  version like 01.2.3 hits the intended mismatch message instead of
  throwing "Invalid version format" out of the comparison.
- Rename the manifest gate's message to name the archive check rather
  than the smoke test, which has not run yet at that point.
- Make the target-version E2E hermetic: pin QWEN_REQUIRE_SIGNATURE,
  QWEN_CODE_LANG, and QWEN_HOME per spawn (witnessed by staging hostile
  ambient values inside the test), re-seed the fixture installation in
  beforeEach so vitest task retries never inherit the post-activation
  tree, and guard the afterAll server-close promises so an early
  beforeAll failure no longer hangs the hook and leaks the scratch root.
- Pin the documented "explicit targets permit same-version reinstalls
  and downgrades" guarantee with performStandaloneUpdate cases that go
  red if the prepared-route staleness check ever moves into the shared
  path.
- Correct the fixture's header comment: local-git-clone classification
  requires the resolved CLI path inside the cwd, not merely a cwd inside
  a git worktree.

* fix(cli): close standalone archive gate review gaps from round 3 (#11486)

Validate the archive's runtime flavor and executable layout against the
installed build before activation, share one version predicate and one
runtime-layout predicate instead of hand-inlined copies, give every gate
predicate a discriminating test, keep the E2E registry sink honest with a
discovery control, and document the manifest identity contract.

* fix(cli): pin the git shim's module scope in the reverse-apply screen test (#11486)

The test shadows git with an extensionless Node shim under tmpdir. Node
derives the module system of an extensionless file from the nearest
ancestor package.json, so a "type": "module" package.json above tmpdir
(the runner image ships /tmp/package.json) loads the CJS shim as ESM:
it dies on "require is not defined" before forwarding to real git, the
restore's ls-files -v spawn never arms the stamp, and the screen test
fails without the screen ever being asked. Pin {"type":"commonjs"} in
the shim directory so the shim loads the way its body is written,
regardless of what sits above tmpdir.

* fix(cli): flavor-aware standalone archive routing and launcher entry-point gate (#11486)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): sanitize --target-version rejection echo and bun test fixture

normalizeVersion throws with the raw flag value interpolated, and the new
--target-version error path echoed it to stderr unsanitized, so terminal
escape sequences (OSC-52, C0/C1, bidi overrides) supplied via argv reached
the user's terminal verbatim. Collapse whitespace and strip escapes,
control and format characters at the write site, mirroring the existing
sanitizeForStderr pipeline, and pin it with an escape-carrying reject case.

Also stage the real bun archive layout in the standalone-update fixture
(bun/bin/bun plus the node/bin/node compat shim installBunRuntime writes)
so the smoke test's two-hop execution chain has a witness: shortening the
staged shim now reds the bun-flavor update test with "Smoke test failed".

* fix(cli): accept pre-v0.23.0 standalone manifests without runtime (#11486)

* fix(cli): address target-version review feedback (#11486)

* fix(cli): keep update recovery guidance and gate win32 test (#11486)

Preserve the updater's manual-recovery command by using a 4,096-character bound for its own failure text; the 500-character bound remains for the untrusted --target-version echo.

Gate the pre-v0.23.0 manifest success-path test with the same win32 skip as its sibling tests, since atomicReplace returns 'deferred' there.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): keep update echo sanitizer at its call site (#11486)

Move the bounded stderr sanitizer back next to the update command so the shared stdio helper keeps its existing surface, and raise the hosted plan wait to the 10s bound the rest of its file uses so the full-suite gate no longer times out under load.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): raise hosted plan wait to the file's 10s bound (#11486)

* test(cli): describe the registry sink's real enforcement (#11486)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): stage the standalone archive layout in the runtime harness (#11486)

applyStandaloneUpdate rejects an extracted archive that lacks bin/qwen and
the bundled runtime, but the harness payload carried neither, so
npm run test:standalone:runtimes:ci died in node/clean before any scenario
ran. Stage the layout create-standalone-package.js ships - bin/qwen plus the
bun flavor's node/bin/node mirror shim - so the fixture matches a real
archive and the update scenarios exercise the gate instead of tripping it.

* fix(cli): gate standalone archives on cli-entry.js (#11486)

The pre-activation layout gate required the installer's launcher and runtime paths, but the repository's standalone-runtime harness repacks a runnable archive without them, so npm run test:standalone:runtimes:ci died in node/clean and the required Test job stayed red. The harness lives in maintainer-only scripts/ territory, so relax the updater's archive gate to the documented v0.18.1 floor: lib/cli-entry.js. smokeTest still runs the bundled runtime, lib/cli.js and lib/cli-entry.js before activation, and isStandaloneInstallDir keeps using the full layout for classification.

Revert the harness edit rejected by the footprint gate, and update the design/user docs and the binless fixture test to the relaxed contract.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): make launcher-less standalone updates launchable (#11486)

The update gate accepts archives from the v0.18.1 floor without the
installer's bin/qwen launcher (the standalone-runtime harness repacks one
that way), but installation detection and the PATH wrapper still required
it. A successful update could therefore leave a tree that neither launched
nor classified as standalone.

Share one layout contract between the gate and detection: the CLI entry
point plus the bundled runtime. Detection still accepts the legacy
launcher-only layout. When a tree has no launcher, ensureBinWrapper writes
a wrapper that execs lib/cli-entry.js through the bundled interpreter and
refreshes a stale wrapper that targeted the removed launcher. Update the
design and user docs and cover the classifier, wrapper and refresh paths.

* fix(cli): resolve a restartable launcher for launcher-less standalone installs (#11486)

Accepting launcher-less install trees made the daemon's one-click update
arm a restart against a hardcoded bin/qwen that such a tree does not have:
activation succeeded, the restart threw ENOENT, and the update was
reported as failed. Resolve the PATH wrapper the updater maintains, which
lives outside the install and is refreshed when an archive ships no
launcher, and fall back to the archive's own launcher; offer manual
instructions when neither exists. Only a wrapper that names the install
may restart it.

Skip the launcher-less acceptance test on win32 like its siblings: its
fixture is Unix-shaped and a Windows host derives win32-x64, which
validateTarget rejects.

---------

Co-authored-by: 西岐 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-bot <[email protected]>
2026-10-31 13:42:54 +00:00
Shaojin Wen 94448d368d fix(verify-pr): keep the mutation matrix off the tree live cells measure on (#15999)
* fix(verify-pr): keep the mutation matrix off the tree live cells measure on

A local round interleaved its mutation matrix with 80 live daemon cells
on one tree. A build inside a mutation row propagated the mutant into
dist/, and the hand-patched repair still carried the mutant expression,
so every live cell and the whole matrix were withdrawn and the round
shipped as inconclusive.

Require a dedicated tree for the mutation matrix, gate every daemon or
dist-level harness launch on a scripted dist-versus-source anchor check
with a mismatch-reporting control, and forbid hand-patching dist to
repair a contaminated build. Pin the new rule in the workflow meta-test.

* test(verify-pr): pin the launch gate's coverage and control clauses

Review on 0d06209b2d showed the four rig-isolation pins left two
clauses of the launch gate unasserted: deleting "covers every PR
anchor" or the mismatch-reporting control still passed 4/4. Pin both
so a future edit cannot silently weaken the gate.

Also name the clean-dist launch gate in the mutation-runner pointer,
so the section that proves a mutant reached dist and the rule that
proves the measurement tree's dist is clean cross-reference each other.

* fix(verify-pr): define the launch gate's reference side and isolate per-tree artifacts

Review on 21014c30d6 (R1-1/R1-2/R1-3, RC1-RC5): the gate compared dist
against an unnamed source, so a self-consistent mutant build passed it;
"PR anchor" was undefined and collided with the mutation-anchor sense;
the control proved liveness, not refusal; the gate had no carve-out for
the producing row's own run; and tree isolation stopped at the checkout
while the workspace links, the shared sandbox tag, and the single Maven
repository stayed host-global.

Name the reference side as a sha256 manifest of each measurement arm's
clean dist recorded before the matrix starts, retire the undefined
anchor term, require the gate to refuse a known-mutant build, exempt the
producing row's own launches, and extend isolation to node_modules, the
sandbox image tag, and the Maven repository. Replace the fragment pins
with the whole bullet pinned inside its section plus the whole pointer
line; a 17-row mutation acceptance goes red on every row, and a
real-file M8 run turned the suite red before a byte-identical restore.

* fix(verify-pr): qualify the matrix-tree copies and name both sandbox levers

Review on ff867ec035: the cp -al recipe dropped the per-package
qualifier the install strategy already requires (root marked 17.0.1
vs packages/core marked 15.0.12 shadow for real), hardlinks share
inodes so a writing row truncates the round's copy, the sandbox clause
named only the launch-side variable while the build reads
QWEN_SANDBOX_IMAGE_TAG and --sandbox-image outranks the export, and the
pin's section slice failed open when the closing heading was renamed.

Carry the qualifier and the never-symlink rule, require real copies
from the store for rows that write under node_modules, name the build
lever and the flag precedence, and assert the slice bounds. The
acceptance matrix grows to 26 rows, all red on mutation including both
heading renames; a real-file rename run turned the suite red before a
byte-identical restore.

* fix(verify-pr): scope the sandbox image per launch and bound the pin slice by the next heading

Review on b99f52a5e3: <tree> sat where a Docker tag must go and the
export's value was never spelled out; a shell-wide export leaks the
matrix image into every later cell; the two cp -al copies disagreed on
when hardlinks are safe (the base build's postinstall step writes under
node_modules); the slice bound was a named heading, so an inserted
section could shelter a moved bullet; and the prose named a build
variable no test pinned at its read site.

Spell a sanitized label and the full image reference, inject the
variable per launch instead of exporting it, carry the write caveat in
both copies with an explicit copy import method (pnpm's auto falls back
to hardlinks without clones), bound the slice on the next heading, and
pin process.env.QWEN_SANDBOX_IMAGE_TAG in build_sandbox.js. The
acceptance matrix grows to 30 rows, all red on mutation; real-file
heading-insertion and variable-rename runs each turned the suite red
before byte-identical restores. Merged main (#15988, #16002); the suite
now runs 197 tests, all green.
2026-10-31 21:08:39 +08:00