Every gate compose-review enforces proves the agents READ the diff — coverage
proves the transcripts, anchors prove the quotes — but none proves the review
could tell good code from bad. Dogfooded: a weak-model run drafted nothing
from its entire roster on a diff where stronger same-condition runs found a
verified blocking Critical, and compose printed a bare, confident
"Verdict: Approve". A reader takes that as evidence of quality when it is only
absence of signal — the most dangerous shape a review verdict can have.
Disclose it, deterministically. When the composed event is APPROVE and the
plan's srcDiffLines — the same field the review topology is chosen from, with
tests, docs and generated files excluded by construction — exceeds 100, the
verdict line names the shape:
Verdict: Approve — low signal: none of the 11 review agents reported a
finding on a non-trivial diff (155 source diff lines)
The event never moves on it: a cap would punish every genuinely clean diff,
and nothing was in fact found. Docs-only and typo-class diffs keep their bare
Approve — there, finding nothing is the expected outcome. The floor sits well
past the typo-fix class (a tiny edit scattered one line per hunk stays under
it) and at a fifth of the smallest diff the topology gate calls big. Both
numbers in the line are the run's own: the roster the plan required (all on
record at APPROVE, or coverage would have capped) and the plan's source-line
count.
Co-authored-by: verify <verify@local>
* fix(cli): stop resize repaint from causing scroll storm (#8004)
Remove the useResizeSettleRepaint -> refreshStatic wiring that wrote
clearTerminal (destroying scrollback) and remounted <Static> on every
settled resize, re-emitting all conversation history in 50-item chunks.
Ghostty's panel-toggle animation exceeds the 200ms debounce, triggering
multiple settle-repaint cycles per toggle -- visible as continuous
scrolling/flickering.
Ink's dynamic region already re-renders on width changes via
useTerminalSize; modern terminals handle scrollback reflow natively.
The full remount is no longer necessary. The now-unused hook and its
test are removed (no remaining callers).
* test(cli): guard resize no-repaint contract against settle-time regression (#8004)
* test(cli): make resize settle regression test non-vacuous (#8004)
The previous test used rerender() which remounts the tree via ink's
ErrorBoundary (measureElement returns undefined → layout effect throws
→ tree unmounted), so the settle debounce never fired and the test
passed regardless. Rewrite to keep the tree alive (measureElement mock
returns a real value) and deliver width changes to the same mounted
instance via a listener pattern. Mutation-verified: fails when the
removed useResizeSettleRepaint hook is restored.
* chore(cli): remove dead useResizeSettleRepaint from eslint legacy filenames (#8004)
---------
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Anthropic's Messages API rejects any tool `input_schema` carrying a
top-level `oneOf`/`allOf`/`anyOf` combinator with a hard 400:
input_schema does not support oneOf, allOf, or anyOf at the top level
send_message's schema used `oneOf: [{ required: ['to'] }, { required:
['task_id'] }]` to express "either a teammate recipient or a
background-task id is required." Since this schema is forwarded
verbatim as `tools[].input_schema` to Anthropic
(anthropicContentGenerator/converter.ts -> convertSchema), every call
to send_message on an Anthropic-backed model (native API, Vertex, or
an Anthropic-compatible proxy) failed outright -- not degraded, fully
unusable.
Verified live against the Anthropic Messages API (claude-sonnet-5):
the schema with `oneOf` 400s with the exact message above; the
identical request with `oneOf` removed succeeds (200, tool_use
returned).
The `oneOf` constraint was redundant defense-in-depth, not the only
enforcement: send_message's execute() already returns a clear runtime
error ("No active team and no task_id provided...") when neither `to`
nor `task_id` is supplied. Dropping the schema-level constraint leaves
that runtime validation intact and unchanged for OpenAI/Gemini-backed
models, while making the tool actually callable on Anthropic-backed
models.
Fixes#7984
Co-authored-by: Palanisamy, Dinesh <Dinesh.Palanisamy@netapp.com>
* feat(channels): add pairing approval management API
* fix(sdk): expose pairing approval types
Re-export the new approval and revocation types from the public SDK entry, and pin the qualified workspace DELETE request body in regression coverage.
---------
Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.com>
* feat(channels): add GitLab polling channel adapter
Poll GitLab todos via @gitbeaker/rest, dispatch notes through the
existing PollingChannelBase pipeline. Key design points:
- action_prompt_template config drives event filtering and metadata
rendering (unconfigured actions are skipped)
- Per-repo cursor (repo[chatId].last_read) as notes window lower bound,
global lastProcessedAt for todo-level dedup
- mark_done after successful processing; failure skips mark_done for
retry on next poll
- Mention gating delegated to base GroupGate (adapter only sets
isMentioned flag)
- First-contact body fallback for todos with no notes (e.g. mention in
issue description)
* fix(channels/gitlab): persist cursor after each successful todo
Call saveCursor() immediately after advancing lastProcessedAt so that
progress is durable even if the process crashes mid-poll. Also removes
the local watermark variable in favor of direct assignment.
* fix(channels/gitlab): persist cursor on every advancement including skips
* fix(channels/gitlab): address review critical issues
- Remove non-functional proxyAgent (gitbeaker doesn't support it)
- Construct repo_url from host + path (API doesn't return web_url)
- Handle directly_addressed action (falls back to mentioned template)
- First-contact fetches target description instead of using todo.body
- Move todo.project dereference inside try block
- Filter confidential notes
- Update channel-registry.test.ts for gitlab entry
* fix(channels/gitlab): address review suggestions
- Warn on connect if action_prompt_template is not configured
- Guard todo.target.iid before use
- Skip paths now mark_done (best-effort) to clean GitLab UI
- Remove postErrorComment (avoids duplicate comments on retry)
- Fetch only first page of notes (desc, maxPages:1, perPage:100)
instead of paginating entire note history
- Extract fetchRecentNotes for single-page windowed enumeration
* refactor(channels/gitlab): simplify to todo.body dispatch, add description mention support
- Remove notes API fetching; dispatch todo.body directly
- Detect description mentions via target_url anchor (#note_ absence)
- Always fetch target description for %description% metadata
- Remove per-repo cursor; dedup via cursor + mark_done only
- Cursor advances regardless of success/failure (no retry)
- Use zod for cursor validation
- Rename template vars to GitLab terminology:
%project% %project_url% %target_type% %iid% %title% %description% %todo_id%
- Support %% escape for literal percent
* docs(channels): add GitLab adapter documentation
- New user guide: docs/users/features/channels/gitlab.md
- Update _meta.ts navigation
- Update developer adapter matrix and SDK list
* fix(channels/gitlab): use correct Issues.show(issueIid, { projectId }) signature
* chore: regenerate NOTICES.txt for new gitlab channel dependencies
* fix(channels/gitlab): address review suggestions
- Add todo.project null guard (item 2)
- Single-pass regex for %% escape + %var% substitution (item 4)
- sendThreadMessage throws directly on undefined threadId (item 5)
- Dedup fetchDescription with per-poll cache (item 6)
- Remove per-todo saveCursor; base class saves after pollOnce (item 7)
- Add undefined threadId test (item 8)
- Expand confidential notes limitation in docs (item 3)
* test(channels/gitlab): add mention tests, directly_addressed coverage, skip assertions, temp cleanup
- New mention.test.ts: 14 cases for testBotMention/stripBotMention/escapeRegex
- Add directly_addressed fallback test
- Skip tests now assert TodoLists.done + cursor advancement
- afterEach cleans up mkdtempSync temp dirs
* fix(channels/gitlab): address review round 4
- Non-mention actions (assigned, etc.) set forceMentioned=true to bypass GroupGate
- Merge dead note-filter tests into single 'skips todo authored by bot'
- Log fetchDescription errors to stderr instead of silent swallow
- Post error comment on issue/MR when handleInbound fails (best-effort)
* fix(channels/gitlab): always force isMentioned=true, remove regex re-derivation
The action_prompt_template config is already the event filter, and
GitLab has already decided the mention when creating the todo.
Re-deriving isMentioned via regex on todo.body causes permanent
message loss when the regex misses (description mention + fetch
failure, group mentions). Always set forceMentioned=true so
GroupGate never drops a todo that passed the template filter.
* fix(channels/gitlab): propagate fetchDescription errors for description mentions
For note mentions, description is metadata-only — fetch failure is
logged and swallowed. For description mentions, description IS the
message — fetch failure now propagates to the outer catch, which
posts the ⚠️ error comment so the user knows to re-mention.
* perf(channels/gitlab): clean up stale todos, skip unnecessary fetchDescription
- Mark stale todos (updated_at <= cursor) as done on each poll to
prevent perpetual re-fetching of pre-existing pending todos
- Skip fetchDescription for note mentions when template does not
contain %description%, saving one API call per todo
- Update docs: stale todo cleanup, error comment on failure
* docs(channels/gitlab): clarify requireMention is bypassed, template is the real filter
* Apply suggestions from code review
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix(channels/gitlab): use todo ID cursor instead of timestamp to eliminate equal-timestamp loss
Timestamp-based cursors (second granularity) could silently destroy
todos sharing the same updated_at as the cursor boundary. Switch to
monotonically increasing todo IDs which are unique and collision-free.
Add initialized flag to preserve first-start drain semantics: pre-existing
pending todos are marked done without dispatch on the first poll cycle.
* fix(channels/gitlab): harden first-poll drain, add ordering tests, fix lockfile
- Replace Math.max(...spread) with reduce to avoid RangeError on large
backlogs (~100k+ todos). Move initialized=true after the drain work so
any throw retries the drain instead of falling through to dispatch.
- Add unit tests: identical-timestamp delivery and id-order-when-updated_at-disagrees
(kills M2 sort mutant).
- Align lockfile: file:../base → ^0.21.0 for channel-base dep.
* fix(channels/gitlab): include dot in mention lookahead for GitLab usernames
GitLab usernames may contain dots (e.g. bot.name). The lookahead
character class inherited from GitHub omitted '.', causing @bot.name
to match as @bot. Add '.' to the negated class.
* docs(channels/gitlab): align docs with ID cursor and drain semantics
- Add first-poll drain as step 2 in How It Works
- Clarify GroupGate always passes (isMentioned forced true)
- Document initialized flag in Known Limitations
* Apply suggestions from code review
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix(channels/gitlab): align package version and channel-base dependency to 0.21.1
Bump version from 0.21.0 to 0.21.1 to match other channel packages after
upstream merge. Pin @qwen-code/channel-base to exact 0.21.1 instead of
^0.21.0, matching the convention used by other published channels.
* fix(channels/gitlab): regenerate lockfile to match package.json versions
Manually add only gitlab-related lockfile entries (workspace, @gitbeaker
packages, transitive deps, channel-gitlab link) without unrelated npm
normalization churn.
* test(channels/gitlab): add regression tests for first-poll drain hardening
Two tests that kill the M1 (Math.max spread RangeError) and M2 (flag
ordering) mutants which survived the original 46-test suite:
- 150k todo drain verifies reduce() handles large backlogs without
RangeError and without dispatching
- Drain throw verifies initialized stays false so the next poll retries
the drain instead of falling through to dispatch
Test file duration: ~40ms → ~170ms.
* docs(channels/gitlab): clarify groupPolicy must be "open" and add runtime warning
The default groupPolicy "disabled" silently drops all mentions — todos are
marked done and cursor advances, but no dispatch occurs. Fix misleading docs
that said "GroupGate always passes" (only true at groupPolicy: "open") and
add a connect()-time warning when groupPolicy is not "open".
* fix(channels/gitlab): correct xcase integrity hash in lockfile
The manually added xcase entry had a typo in the sha512 hash (ys → ks),
causing npm ci EINTEGRITY failures in CI.
* fix(channels/gitlab): correct requester-utils integrity hash in lockfile
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(channels/gitlab): allow groupPolicy "allowlist" in warning and docs
The groupPolicy warning and docs incorrectly stated that groupPolicy
must be "open". In reality "allowlist" with the project listed also
works because isMentioned is forced true and GroupGate only requires
the group to be listed. Also fix the inaccurate "no error is logged"
claim — ChannelBase logs preflight rejected reason=group_disabled.
Fixes R5-🟡3 from PR #7862 review.
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(web-shell): add split pane header action slot with overflow
Let hosts render per-session actions in each split pane header, collapsing them into a … menu when the pane is too narrow.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(web-shell): add pane header actions PR screenshots
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): tighten pane header overflow measurement
Drop the per-render children effect dependency that rebuilt ResizeObserver during streaming, and reserve workspace-tag width when computing available header space.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): address pane header overflow review blockers
Mount host actions in only one tree, and wrap overflow entries as DropdownMenuItems so Radix selection and keyboard navigation work.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): keep pane header actions alive across overflow
Flatten Fragment host actions before building the overflow menu, and keep the same host instances mounted when collapsing so stateful actions are not reset.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): address pane header overflow review suggestions (#7808)
* fix(web-shell): proxy overflow clicks via action slots
Wrap host pane actions in stable slots so the overflow menu can activate interactive descendants without requiring opaque custom components to forward internal data attributes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): address overflow menu review suggestions (#7808)
* fix(web-shell): harden pane header overflow actions (#7808)
Restore the 8px gap between the built-in maximize/close controls, ignore
aria-hidden glyphs when labelling overflow items, omit non-interactive
children from the overflow menu, and document the popover constraint on
renderHeaderActions. Refreshes the design doc to match the mount-once
implementation.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
* feat(hooks): add security.allowPrivateNetworkHooks to bypass SSRF range checks for trusted scopes
HTTP hooks hard-block all private/link-local address ranges via ssrfGuard,
which makes them unusable in platform-managed environments where the hook
receiver is a first-party, VPC-internal endpoint (e.g. an internal API
gateway resolving to 172.16.0.0/12).
Add an opt-in setting, security.allowPrivateNetworkHooks (default false),
that skips the SSRF IP-range checks in urlValidator.isBlocked (literal IPs)
and validateResolvedHost (literal + post-DNS-resolution paths).
Security properties:
- Honored only from User/System/SystemDefaults scopes; the value is
stripped from Workspace settings during the merge (with a startup
warning), so a cloned repository can never self-grant the bypass.
- BLOCKED_HOSTS (169.254.169.254, metadata.google.internal, ...) remains
blocked even when the flag is on.
- Default false keeps every code path byte-for-byte compatible with
current behavior; bare/safe mode forces it off.
* fix(hooks): enforce metadata endpoint blocklist regardless of allowPrivateNetworkHooks
Address review findings on #7968: with the flag on, cloud metadata
endpoints were reachable through gaps in the relaxed checks.
- ssrfGuard: add METADATA_IPS (169.254.169.254, 100.100.100.200) and
isMetadataAddress(), which normalizes IPv4-mapped IPv6 forms
(::ffff:a9fe:a9fe, ::ffff:6464:64c8, ...) via the existing
extractMappedIPv4/expandIPv6Groups helpers.
- urlValidator.isBlocked: BLOCKED_HOSTS matching and the literal-IP
isMetadataAddress check now run unconditionally; only the general
range check (isBlockedAddress) is relaxed by the flag.
- httpHookRunner.validateResolvedHost: no longer returns early with the
flag on — DNS resolution still runs and resolved addresses are checked
against isMetadataAddress, so a hostname resolving to a metadata
endpoint is blocked. DNS failures still defer to fetch, as before.
- settings warning text now lists User/System/SystemDefaults, matching
the schema and docs.
- docs: precise wording — the flag relaxes only range checks; metadata
endpoints stay blocked in all serialized forms and after DNS resolution.
The flag now opens RFC1918/CGNAT/link-local ranges only; cloud metadata
endpoints (169.254.169.254, 100.100.100.200 in any form, plus the
BLOCKED_HOSTS hostnames) are unreachable in every configuration.
---------
Co-authored-by: 欢伯 <ri.xur@alibaba-inc.com>
* fix(cli): MCP prompt completion no longer blocks Enter for optional params (#7991)
The completion handler treated all unused arguments (required + optional)
as completion suggestions with auto-appended `="`, making optional params
look mandatory. After selecting a prompt name, pressing Enter would
auto-append `--input="` instead of executing the prompt.
Two changes:
1. Completion handler now parses named args directly instead of using
parseArgs() (which returns Error on missing required args — the normal
state during tab completion).
2. Only required unused args are suggested as completions. When all
remaining unused args are optional, the completion list is empty so
Enter executes the prompt with defaults.
* fix(cli): restore optional and positional MCP prompt completion discovery (#7991)
* fix(cli): share named-arg regex and cover mixed MCP prompt completion (#7991)
* fix(cli): suggest optional args mid-keystroke and test multi-word positionals (#7995)
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-bot <qwen-code-bot@users.noreply.github.com>
* fix(cli): reserve Ctrl+Shift+C for terminal copy, not quit/clear
Ctrl+Shift+C is the standard terminal copy shortcut. The QUIT and
CLEAR_INPUT bindings matched it because they only constrained ctrl
and left shift unspecified, which the matcher ignores. In Kitty-
protocol terminals the escape-hatch check also caught Ctrl+Shift+C
(ESC[99;6u) since it tested ctrl+name without excluding shift.
Add shift: false to both bindings and !key.shift to the escape-hatch
condition so Ctrl+Shift+C passes through to the terminal for copy.
Plain Ctrl+C (shift undefined or false) is unaffected.
Fixes#8006
* test(cli): guard Ctrl+Shift+C paste escape-hatch against !key.shift mutation
The existing Kitty Ctrl+Shift+C test passed identically with or without
the `!key.shift` condition in the Ctrl+C escape hatch, so a regression
making Ctrl+Shift+C clear a stuck paste would ship undetected. Add a
test that enters a stuck keypress-level paste and asserts Ctrl+Shift+C
is buffered as paste content rather than dispatched, which fails when
`!key.shift` is removed.
* fix(cli): remove dead !key.shift escape-hatch guard (#8011)
The KeypressContext.tsx escape-hatch guard added in bb86d57 is
unreachable: Node readline never produces a Key with ctrl=true,
name='c', and shift=true for any terminal encoding (kitty,
modifyOtherKeys, or legacy 0x03). Mutation test M2 confirmed
reverting this hunk has zero effect on the test suite.
Remove the guard and the two tests that exercised it. The
keyBindings.ts shift:false fix (the load-bearing change proven by
M1) and its keyMatchers tests are untouched.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The workflow `timeout-minutes` and the script's `totalTimeoutMs` are
defined in separate files with no shared constant. If they drift apart
silently and the budget exceeds the step timeout, the runner SIGKILLs
the step and even the fallback release notes are lost.
Add a cross-file assertion so any future change to either value that
breaks the invariant fails the test immediately.
Suggested-by: wenshao in PR review.
* feat(triage): sponsored /verify for external PRs on ephemeral runners
/verify used to require the PR AUTHOR to hold write access, because the
lane executes the author's code on the persistent ECS pool — which
excluded external contributors' PRs entirely (16 of 38 open PRs
eligible when measured on 2026-07-28), and those are the PRs where
sandboxed evidence is worth the most. The practical alternative was a
maintainer running the verification on their own machine, which is
strictly worse: personal credentials, SSH keys, and no isolation at
all. The risk was not avoided, only relocated.
The author's permission now ROUTES instead of gating, using the same
conditional runs-on pattern the authorize/triage jobs already use for
fork PRs:
- write-access author -> the persistent ECS pool, unchanged;
- external author -> a SPONSORED run on an ephemeral GitHub-hosted VM
(destroyed after the job), free on a public repository.
The commenter gate is unchanged: only a write-access maintainer can
trigger either lane, so runs stay maintainer-metered. Their comment is
the sponsorship, and authorize snapshots the head OID at that moment;
the resolve step refuses to run any other commit, so code pushed while
the job queues is never executed on the sponsor's approval. On the ECS
lane the execution-time re-check keeps its old meaning (the author's
write permission is the control there, and it can be revoked while
queued); its refusal now names the sponsored alternative.
Before anything executes on the hosted lane, a pre-execution risk
screen reads the PR diff as data: mechanical checks (added npm
lifecycle scripts, dependencies resolving outside registry.npmjs.org,
package-manager config changes, long opaque single-line content) and a
model screen, every arm failing CLOSED — an unfetchable diff, missing
screen config, a model error, and an unparseable reply each refuse the
run. Reasons posted to the PR are fixed strings chosen by the
workflow, never diff text, so a crafted diff cannot steer the bot's
comment. The screen is defense in depth, not the control: what bounds
a malicious PR is the ephemeral VM. The model can be fooled;
the machine being destroyed cannot.
The ECS kill switch no longer strands the hosted lane (an ephemeral
runner does not depend on the pool), and the authorize-side denial
notice for external authors is gone — the case it explained no longer
denies. The 2b-bis skill guidance now offers the sponsored run instead
of declaring the lanes unavailable, with an explicit warning that an
external author's report is adversarial input to be read like the
fork's own CI logs.
Mutation-verified 8/8, each with landing proof: dropping the commenter
gate, inverting the routing, dropping the OID snapshot, letting a
permission-API failure route instead of deny, dropping the head-moved
check, dropping the lifecycle-script check, tolerating an unparseable
model reply, and pinning the hosted lane back to ECS each turn a test
red. The screen tests run the real resolve-step text against a stubbed
gh and a live local HTTP server standing in for the model endpoint —
in a separate process, because spawnSync blocks the test runner's
event loop and an in-process server would deadlock into the fetch's
120 s abort instead of testing anything.
actionlint category diff vs main: -3 SC2016:info (the removed denial
step), +3 SC2129:style (new GITHUB_OUTPUT groups); both non-gating.
* docs(triage): persistence, not lateral movement, is the ECS threat
The maintainer confirmed the ECS runners are network-isolated, so the
comments justifying the hosted routing now name the decisive risk
correctly: machines are reused and the next run arrives carrying
credentials that planted artifacts could harvest. Lateral movement was
never the load-bearing argument; persistence is, and it is the one
threat a destroyed VM removes without depending on sweep completeness.
* feat(triage): run external /verify on ECS behind a workspace wipe
Maintainer decision (2026-07-29): external contributors' PRs run on the
persistent ECS pool like everyone else, not on ephemeral GitHub-hosted
VMs. The pool is network-isolated, and the realistic alternative — a
maintainer verifying on a personal machine with real credentials and no
isolation — is worse than a hardened shared runner.
That removes machine destruction from the design, so the author's
permission now selects CONTROLS rather than a machine:
trusted -> unchanged: write permission is the control, re-checked at
execution time, head may move freely.
external -> pinned head OID (the sponsoring comment approves the code
that existed then), pre-execution risk screen, and a full
workspace wipe.
The wipe is what replaces destruction. The residual ECS risk for
untrusted code is PERSISTENCE: machines are reused, and the next run
starts by handling credentials (CI_BOT_PAT in the resolve step, the
model key in the proxy). The existing sweep is an allowlist of vectors
we have thought of, and an adversarial author gets unlimited attempts to
find one we have not — so external runs delete the workspace outright
before checkout instead of sweeping it. Deny-by-default costs one cold
clone and removes hooks, worktree config, planted dotfiles, staged
artifacts and out-of-tree symlinks in one move, known or not. Scope is
stated in the step: RUNNER_TEMP and /tmp also persist, and the steps
reading them already rm -rf their own subtrees; the container is
recreated per job, so /home/node and its npm cache are not cross-run
state.
The screen's status changes with the machine: on ephemeral hardware it
was a cost-saver, here it is load-bearing — but still not sufficient
alone, and the comment says so, naming the wipe and the sweep as what
actually bounds a screened-but-hostile diff.
Mutation-verified 6/6: removing the wipe, moving it after checkout,
applying it to every run, tolerating a non-empty workspace, dropping the
suspicious-path guard, and classifying an external author as trusted
each turn a test red.
Also fixes a live hazard in the guard's own test. It passed real paths
(/, /usr, /root) to a live `rm -rf`, which is safe only while the guard
exists — so it detonates precisely when the guard is removed, which is
when it is meant to protect you. It did: a mutation run that deleted the
guard spent six minutes attempting to delete / before being killed.
macOS permissions absorbed it and nothing was lost, but that outcome was
luck, not design. `rm` is now stubbed to a recorder on PATH, so the
destructive primitive cannot fire from the test suite under any edit,
and the assertion is on the decision — with the guard gone the recorder
shows the attempted delete and the test fails, having deleted nothing.
* fix(triage): close two screen bypasses and a vacuous routing assertion
Review round on #7985 found four issues; all four were real.
1. npm lifecycle alternation stopped at the install lifecycle, but npm
runs pre/post hooks for ANY `npm run <script>`. This job runs
`npm run build` and the agent runs the PR's suites, so a `prebuild`
or `pretest` executes exactly like a `postinstall` and walked past
the mechanical screen. Added build and test to the alternation.
2. The off-registry check excluded any line CONTAINING
`registry.npmjs.org`, so a lookalike host — registry.npmjs.org.evil.com,
or an @-userinfo form — read as npmjs and passed. The tarball's own
postinstall lives inside the tarball, never in the diff, so this arm
was the only mechanical defense against it. The exclusion is now
anchored to scheme + host + '/'.
3. A routing assertion read `.runner`, a key from the earlier
ephemeral-VM design that `gate()` never returns, so it was
`expect(undefined).toBeUndefined()` — green no matter how badly the
trust level leaked into /triage or /tmux. Asserts the real keys now,
on both non-verify paths.
4. Three screen arms had no scenario at all: package-manager config,
the long-opaque-line awk, and the unconfigured-model fail-closed
path. All three are covered, each with a negative control so the arm
is not merely refusing everything — the genuine registry URL and a
700-char lockfile integrity line must still pass.
Mutation-verified 6/6, each failure naming the payload it let through:
reverting the alternation (`prebuild was not flagged`), un-anchoring the
registry match (`registry.npmjs.org.evil.com was not flagged`), breaking
the package-manager grep (`.yarnrc was not flagged`), raising the awk
threshold, letting the unconfigured path fall through to clear, and
leaking verify_trust into /triage — which is exactly what the vacuous
assertion could never see.
93/93 tests; prettier, eslint and shellcheck clean.
* fix(triage): close post-run persistence gap and screen bypasses (#7985)
Maintainer review found the wipe only ran BEFORE external code, leaving
the next pool job to meet the allowlist sweep alone. Add a post-run wipe
with `if: always()` so a cancelled or timed-out external job still
cleans up.
The model screen silently truncated at 200 KB — a fail-open in a
fail-closed step. Refuse oversized diffs mechanically before the model
call.
The screen prompt still described an "ephemeral, credential-free
sandbox" from the deleted ephemeral-runner design, calibrating the
model for leniency the persistent pool does not warrant. Describe the
actual environment. Also fix the stale "hosted lane" comment.
Three cheap mechanical-screen hardenings from the review: drop the
root anchor on the package-manager config grep (a nested .npmrc is
just as dangerous), match `rename to` headers (a rename-only hunk
emits no `+++ b/` line), and relax the opaque-line awk from
"no space in the line" to "no space-free field >= 600" so a single
space anywhere is no longer a bypass.
94/94 tests; build, typecheck, lint clean.
* test(triage): cover the unfetchable-diff fail-closed screen guard (#7985)
---------
Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
* test(integration): migrate flaky E2E tests to fake-openai-server
Migrate 39 real-model test cases to use deterministic fake-openai-server
scripting, eliminating model output variance as a failure source.
- tool-control.test.ts: 23 cases migrated (tool filtering, permission
denial, canUseTool routing, priority rules)
- abort-and-lifecycle.test.ts: 15 cases migrated (abort mechanics,
lifecycle, cleanup, debug output)
- list_directory.test.ts: 1 case migrated (CLI TestRig with env injection)
channel-plugin.test.ts (3 cases) is intentionally left on real model —
it tests the full WebSocket→AcpBridge→model pipeline and belongs in the
nightly smoke layer per #7616.
Closes#7616
* test(ci): move channel-plugin to nightly + relax assertions
channel-plugin.test.ts tests the full WebSocket→AcpBridge→model pipeline
with real model inference. Its assertions on specific model output
('4', 'pineapple', '50') are inherently non-deterministic.
- Exclude from post-merge E2E jobs (Linux + macOS)
- Add dedicated nightly-only job with continue-on-error
- Relax assertions: verify response is non-empty rather than matching
specific model output content
- Add retry: 2 to each test case
* fix(test): use streaming chunks for abort test, restore comment
- abort-and-lifecycle 'should handle abort during query execution':
switch from non-streaming content to contentChunks so the abort
signal reliably lands while the response is still streaming
- channel-plugin: restore existing comment per AGENTS.md convention
* test(integration): address fake server review feedback
* test(integration): trim fake server review assertions
* test(integration): tighten fake server cleanup
* test(integration): fix permanently-failing stdin-close case and harden list_directory against user settings
The stdin-close case's fake server needle contained a raw quote that
JSON.stringify-escaped transcripts can never match, and the scripted
write_file was rejected by prior-read enforcement; the script now keys
off a quote-free marker and reads before writing. list_directory now
pins auth via CLI flags so a developer's ~/.qwen/settings.json cannot
silently route the run to a real model endpoint.
* test(integration): tighten fake server review regressions
* test(integration): guard abort assertions
* test(integration): fix abort timer race and dead coreTools assertions
* test(integration): address fake server review findings
* test(integration): isolate fake fast model settings
* test(e2e): simplify isolated test setup
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Dogfooded on a live review: with ~2.7G free, `npm ci` on this monorepo ran 33
seconds and died on ENOSPC — and the now-full disk went on to fail every agent
scheduled after this command. Verification and reverse-audit agents could not
even spawn, so a Critical that nine agents independently reported ended the run
capped at Comment because nothing could verify it. A disk a command fills is
not a failure that stays contained to that command.
build-test already treats "cannot finish in time" as an infrastructure result:
the deadline skips the command and discloses it, and the agent brief routes
such notes to informational, never a finding. "Cannot fit on the disk" is the
same class of result, discoverable before the command runs instead of 33
seconds into it.
Add a free-disk preflight at two floors: 3 GiB before `npm ci` (the installed
tree is ~1.4G and npm stages cache and temp writes on the same filesystem
while materialising it) and 1 GiB before the build phase (dist/ and
tsbuildinfo write far less; the floor exists so a compile cannot be the thing
that fills the disk). Two floors deliberately: a warm tree at ~2G free skips
the install anyway and must still be allowed to build. Below a floor the
command is skipped with the same disclosure shape as a deadline skip. Where
statfsSync is unavailable the preflight passes — it exists to prevent
failures, not to invent them.
Co-authored-by: verify <verify@local>
* feat(triage): lead the verify comment with a qualitative verdict, fold the Chinese
Maintainer feedback on the first 14-run day of the lane, pointing at the
report on PR #7836: give a plain pass/no-pass, and make the layout
English-by-default with the Chinese folded (the repo's PR-body
convention), instead of doubling every head line.
Three changes:
- every headline leads with a qualitative call — ✅ passed / ❌ not
passed / ⚠️ inconclusive / ⚠️ incomplete — ahead of the lane taxonomy.
The mapping is honest about what each outcome can claim: only agent
verdicts judge the code; a crashed, timed-out, or verdict-less run
renders as incomplete/inconclusive, never as a pass or a fail, and a
test pins that no process-outcome arm carries ✅ or ❌.
- the Chinese version folds into one <details> whose SUMMARY carries the
qualitative verdict — collapsed, a Chinese reader still sees
通过/不通过 in one line. English scope and assertion count stay
unfolded; both language assertion lines are built from the same
validated triple, so inconsistent JSON still suppresses both.
- the #7836 root cause gets a Hard rule in the verify-pr skill. That
report said "merge-ready — the 7 failures are all expected A/B
base-cell failures proving the tests load-bearing" while
assertions.json said fail:7, so the publisher's trust rule (merge-ready
requires fail==0) correctly refused it and the headline degraded to
"no usable structured verdict". Both sides told the truth about
different questions. The rule fixes the semantics: an A/B control cell
is an assertion that the base arm FAILS — when it does, that assertion
PASSED. fail counts only unexpected outcomes, which is what makes a
qualitative headline trustworthy at a glance.
Mutation-verified 6/6: dropping the qualitative prefix, collapsing all
Chinese quals onto one string, unfolding the Chinese scope, giving
timeout a ✅, dropping the folded Chinese assertion line, and deleting
the new Hard rule each turn at least one test red. The fifth mutation
initially reported "landed: False" — the regex never matched and the
file was untouched, so the green result proved nothing; re-applied as a
line filter with a removed-block count, it kills two tests.
One new shellcheck SC2016 info finding (backticks inside a single-quoted
Chinese printf), same shape as the 11 pre-existing ones; info-level,
does not gate.
* test(triage): pin the dropped verdict arms and prepare-failure glyphs (#7974)
Restore rendering coverage for the reachable infra-error and unknown catch-all case arms in the qualitative-headline bijection, and assert the prepare-failure path's qualitative glyphs, Chinese fold summaries, and the Chinese retry clause so a swapped emoji or dropped PREPARE_ATTEMPTS_ZH interpolation can no longer ship undetected.
* fix(triage): migrate all weak headlines, explain merge-ready mismatch (#7974)
* fix(triage): fold Chinese mismatch explanation, differentiate distrust warnings (#7974)
---------
Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: Qwen Code CI Bot <qwen-code-ci-bot@users.noreply.github.com>
* fix(cli): map Kitty Super (Command) modifier to meta
Kitty-protocol terminals forward Cmd+C as a CSI-u sequence whose modifier
parameter carries the Super bit (8), which the parser silently dropped. The
key then parsed as a bare printable "c" (meta: false) and leaked into the
input box even though the terminal performed the copy itself. Fold the Super
bit into the emitted meta flag at every Kitty modifier decode site so
Super-modified keys are no longer inserted as text.
Fixes#7990
* test(cli): cover Super-bit fold on reverse-tab and functional-keys paths (#7996)
* test(cli): make functional-key Super-bit case load-bearing
Use Home (ESC [ 1 ; 9 H) instead of Up arrow (ESC [ 1 ; 9 A): readline
claims modified arrows before the Kitty arrowPrefix decoder, so the arrow
case passed even without the Super-bit fold. Home exercises the decoder
and fails on unfixed code.
---------
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
* ci: add Windows runner smoke test
* ci: use built-in Windows PowerShell
* ci: secure Windows runner smoke trigger
* ci: run full test suite in Windows validation
* ci: keep Windows validation manual
The DingTalk interactive cards change (#6930) added concurrent
session-cancellation coalescing to DaemonSessionClient, growing the
minified browser daemon bundle to 180295 bytes — 71 bytes over the
176KB budget, breaking npm run build on main. Bump the budget to
177KB following the established pattern.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(release): skip notes-start-tag when previous release diverges from target (#7969)
* style(release): fix prettier formatting in release workflow (#7969)
* fix(release): warn when notes-start-tag is skipped for a divergent tag (#7969)
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* fix(serve): allow bounded reads of large text files
* fix(serve): bound large-text reads by scan cost, not by which knob was set
Follow-up to the bounded large-text read path. Three changes:
Gate on any explicit window argument, not on `limit`. Gating on `limit`
had the cost model backwards in both directions: `{ line: 900_000_000,
limit: 20 }` was admitted despite walking the whole file, while
`{ maxBytes: 4096 }` — satisfiable from the first 4 KiB — was refused. A
read with no window argument at all still fails, since a caller that
believes it holds the whole file may write it back truncated.
Add MAX_TEXT_SCAN_BYTES (8 MiB). MAX_READ_BYTES caps what a read
returns; nothing capped what it cost. Line offsets are resolved by
scanning from byte 0, so a query param could turn into an
uninterruptible multi-second scan of an arbitrarily large file — and on
Windows hold a read handle for that span, blocking renames and deletes.
Past the budget the read is refused with `file_too_large` pointing at
readBytes, which reaches any offset in O(1).
Tolerate appends on streamed windows. Requiring whole-file size/mtime
stability after reading a prefix rejected reads whose returned bytes
were still valid, and the case it rejected — tailing a live log — is the
one this path exists for. Streamed windows now assert inode identity
plus "did not shrink"; truncation and replacement are still rejected.
Also: non-UTF-8 large text now returns `binary_file` rather than
`file_too_large`, so a client retrying on 413 with a smaller window
can't loop forever; and `readFileWithLineAndLimit` throws instead of
silently ignoring a caller-supplied `fileHandle` on the by-path
fallback.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(serve): harden large text range snapshots
Treat caller-owned file handles as bounded streaming reads, cap them to the captured file size, and reuse the chunk buffer.
Restore strict Serve snapshot stability and align returned-slice metadata with the full-snapshot path.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(serve): make large text ranges snapshot-safe
* fix(serve): harden large-text ctime tests and document buffer reuse (#7947)
Address review feedback on the large-text range read PR:
- Pause before restoring mtime in the two ctime-dependent mutation tests so the change-time advances past the pre-read snapshot even on coarse-resolution filesystems, removing a latent flake in the same-size-overwrite precondition. The assertions are unchanged.
- Document at the readFileHandleChunks yield site that the 512 KiB buffer is reused across iterations, so yielded views must be decoded or copied before advancing the generator.
* docs(serve): soften same-size rewrite guarantee to coarse-clock best-effort (#7947)
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
On Windows (pasteWorkaround path), shouldFlushRawDataAsPaste
misclassifies raw stdin data containing both a carriage return (0x0d)
and an SGR mouse sequence as pasted content. The synthetic paste
wrapper sets isPaste=true in handleKeypress, which discards SGR mouse
data via resetSgrMouse(), breaking wheel scrolling in VP mode.
This commonly triggers when Enter (\r) and a subsequent mouse wheel
event arrive in the same stdin chunk — a frequent occurrence on
Windows Terminal. macOS is unaffected because handleStdinData does
not use this heuristic.
Skip the paste heuristic when the buffer contains ANSI escape bytes
(0x1b) so SGR mouse sequences always reach readline for proper
parsing and dispatch to mouse subscribers.
Closes#7964
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
* fix(cli): correct hardware cursor off-by-one in fullscreen mode
In fullscreen mode Ink omits the trailing newline, so after writing
output the terminal cursor sits ON the last line rather than one past
it. buildCursorSuffix assumed the latter and emitted one extra
cursorUp, placing the hardware cursor (used for IME positioning) one
row above the software block cursor — visible as an extra white
rectangular segment protruding above the input line.
Pass a hasTrailingNewline flag through buildCursorSuffix,
buildReturnToBottom, buildCursorOnlySequence, and
buildReturnToBottomPrefix in the Ink patch so both the standard and
incremental log-update renderers compute the correct cursor movement.
Closes#7980
* fix(cli): revert incorrect hasTrailingNewline in buildReturnToBottom
Self-review caught that buildReturnToBottom does NOT need the
hasTrailingNewline adjustment: its previousLineCount argument comes
from str.split('\n').length, which already includes the trailing
empty element when the output ends with '\n'. The original formula
(previousLineCount - 1 - y) is the exact inverse of the corrected
buildCursorSuffix in both fullscreen and non-fullscreen modes.
Only buildCursorSuffix needs the flag, because its visibleLineCount
argument excludes the trailing empty element.
* fix(cli): sync cursor-helpers.d.ts and add fullscreen cursor-only test (#7998)
---------
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
* fix(web-shell): preserve session URL context
* fix(web-shell): keep daemon token out of the session URL (#7926)
Restore the daemon token stripping that was dropped alongside the
base-path fix: removeDaemonTokenFromUrl() on startup and the ?token=
delete in replaceStandaloneSessionUrl. The ?token= query path is still
supported for backward compatibility, so without stripping it leaks into
the address bar, history, access logs, and Referer headers.
Also extract the session pathname building into buildSessionPathname()
with unit tests covering root/sub-path deployments, the no-session case,
trailing slashes, and id encoding.
* fix(web-shell): anchor session URL parser to agree with writer (#7926)
Extract parseSessionId() next to buildSessionPathname() and anchor it to the last /session/<id> segment so the parser agrees with the greedy writer. Previously a base path ending in a session segment produced /app/session/session/<id>, which the first-match parser read back as the literal id "session". Add round-trip and trailing-slash coverage.
* test(web-shell): cover parseSessionId malformed-encoding catch branch (#7926)
* fix(web-shell): preserve base path in split-view URL (#7926)
---------
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix(core): mark folders the item budget never expanded
A folder is pushed into its parent's subFolders and queued for expansion.
When it is later dequeued and the maxItems budget is already spent, the
BFS skips it with a bare `continue`, leaving hasMoreFiles and
hasMoreSubfolders unset. It then formats as a bare leaf -- byte for byte
how a genuinely empty directory formats.
So the same tree was described differently depending only on the budget.
The existing suite shows it: `level1/level2/level3/file.txt` renders
level3 with its file at maxItems 10, and as an empty directory at
maxItems 3. The subfolder test is worse -- folder-0 through folder-3 each
contain child.txt and all four render as empty.
The parent's hasMoreSubfolders flag does not cover this. It means "I could
not add every subfolder", and these folders were added; it is only the
expansion that never happened.
Set hasMoreSubfolders on the skipped node so it renders with the
truncation indicator. Only that one flag is set: formatStructure emits an
indicator per flag, and this one alone produces the single trailing `...`.
Three existing expectations encoded the old rendering and are updated.
* docs(core): state both meanings of hasMoreSubfolders on the interface
This PR widened the field: it already meant "subfolders were truncated" and
now also means "the item budget ran out before this folder was read." The
interface comment only described the first, so a second consumer reading just
the type could gate rendering on `subFolders.length > 0` and silently drop the
indicator for never-expanded folders, reintroducing the same
looks-like-an-empty-directory bug in a new path.
The AI release-notes generator timed out on every batch in the v0.21.1
finalize run on a GitHub-hosted runner (US, Azure westus3) hitting a
remote LLM endpoint. The 60s per-request timeout was too close to the
edge: two successful requests took 54.2s and 56.7s, and every other
batch hit the 60s abort. The 12-minute total budget was consumed by
retries on those timeouts, and the circuit breaker opened after 3
consecutive failures, skipping highlights entirely.
- timeoutMs: 60s -> 180s — give the model enough headroom to generate
a full JSON response over a high-RTT cross-region connection.
- totalTimeoutMs: 12min -> 30min — ~140 PRs at 8 per batch is ~18
batches; at ~90s each that's ~27 minutes of model time.
- batchSize: 12 -> 8 — fewer entries per prompt means the model
generates less per request, returning faster and reducing the
chance of a single slow request dragging the whole batch.
- workflow timeout-minutes: 15 -> 35 — match the new 30min budget
plus a margin for git/npm setup.
No changes to retry logic, circuit breaker, or prompt shape.
* fix(core): improve ripgrep runtime reliability
Make ripgrep failures distinguishable from true no-match results so the
model avoids unsafe conclusions from partial or failed searches. Add a
narrow recovery path for confirmed worker-thread EAGAIN failures.
- Retry confirmed thread EAGAIN once with a single worker thread
- Treat exit code 1 as no-match only when both streams are empty
- Mark partial runtime results as incomplete, separate from display limits
- Add privacy-safe telemetry and focused coverage for recovery behavior
# Conflicts:
# packages/core/src/utils/ripgrepUtils.test.ts
* fix(core): restore ripgrep runtime recovery tests
Restore the runRipgrep coverage that was lost during the rebase and keep
EAGAIN detection aligned with the runtime reliability design.
- Re-add coverage for strict no-match handling and incomplete output
- Verify single-thread retry behavior for confirmed EAGAIN failures
- Keep spawn, cancellation, timeout, and max-buffer paths covered
- Treat os error 11 as the short EAGAIN marker documented by the plan
* docs(core): document ripgrep recovery boundaries
Clarify the non-obvious runtime reliability edges around ripgrep recovery so
future changes preserve the intended narrow behavior.
- Document why only confirmed worker EAGAIN failures are retryable
- Explain incomplete-output handling for interrupted ripgrep output
- Note the privacy boundary for runtime recovery telemetry
* fix(core): correct exit-1 no-match gate for --json summary output (#7888)
* test(core): remove duplicate mockReset and add telemetry coverage (#7888)
* fix(core): address review feedback on ripgrep recovery semantics (#7888)
- Fix stale `truncated` property in test mock to match RipgrepRunResult
- Narrow `incomplete` flag to genuinely interrupted executions only
- Add test for exit code 1 with both stdout and stderr
- Add test for EAGAIN retry producing partial stdout
- Alias RipgrepRuntimeRecoveryFailureKind to RipgrepFailureKind
* docs(core): align exit-1 no-match spec with stderr-only gate (#7888)
* fix(core): mark exit-failed ripgrep searches with stdout as incomplete (#7888)
---------
Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
* feat(external-context): add submitted-prompt auto recall
Add an opt-in Hook-only profile that derives bounded retrieval queries from submitted prompt provenance while preserving the existing on-demand MCP contract.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(external-context): harden auto recall query sanitization (#7877)
Address review feedback on the submitted-prompt auto recall Hook:
- Bound the sanitizer input before the redaction regexes run so a
worst-case prompt cannot drive the assignment regex into quadratic
backtracking that blocks the event loop past the wall-clock budget.
- Test the whole assignment for secret names so a leading label such as
"Deploy failed:" can no longer claim the match and leak an api_key=.
- Skip the interactive E2E under container sandboxes (docker/podman),
matching the cron-interactive precedent.
- Restore real undici coverage for a malformed proxy environment value.
- Make the wall-clock-budget test exercise the internal timer rather than
the provider timeout, and give the backtracking regression test a shape
that actually backtracks.
- Clarify that the v2 top-level timeoutMs applies only to the on-demand
MCP path, and note session-lifetime context accumulation in the design
doc.
* fix(external-context): complete secret redaction, guard MCP config version (#7877)
Anchor the secret keyword to the name that owns the separator so a leading
prose label can no longer claim the match. This redacts spaced separators
(api_key = sk-...) and inline JSON ({"api_key": "..."}), and stops
over-redacting ordinary prose such as "readme: token refresh flow".
Also reject non-version-1 configs in runMcp with a clear startup error so an
auto-recall (v2) config cannot silently expose a second retrieval surface.
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Post-merge measurement of #7917, one day in: 9 eligible PRs, two
considered-and-declined mentions (both correct calls), zero positive
recommendations. The one clear behavioural candidate — #7947, bounded
reads of large text files — wrote "Not verified: Windows and Linux
manual runs (author tested on macOS only)" in its own Stage 2 comment
and never named a lane.
That is the failure shape worth fixing: the judgement-based rule ("when
neither static review nor 2b substantiates it") failed exactly where
the comment had already written the gap down in so many words. The
model judged that pending CI would cover it; a green suite proves the
tests pass, not that the untested behaviour holds.
So the trigger is now textual, not judgemental: before posting, grep
your own draft. A sentence of the shape "not verified", "author tested
on one platform only", or "author's claim, not independently re-run"
IS the trigger — the 2b-bis line is that same sentence with the remedy
attached, and omitting it means telling the maintainer what is missing
while withholding the one command that would supply it. Pending CI
does not lift the trigger. The two legitimate skip cases (nothing
behavioural to settle; author lacks write) are unchanged, and the rule
is ordered before them so they read as outs from the requirement, not
the requirement as an out from them.
The trigger phrases are verbatim from real comments: "not verified"
and "author tested on macOS only" from #7947, "author's claim, not
independently re-run" from #7951.
Mutation-verified 3/3: dropping the trigger paragraph, moving it after
the skip cases, and dropping the pending-CI sentence each turn the test
red. n=1 is thin evidence for a behavioural rule change — but this rule
is text-matching, not probability-weighing, so it cannot overfit to the
sample that motivated it.
Co-authored-by: wenshao <wenshao@example.com>
On reused GitHub-hosted runners, a previous job (e.g. verify/tmux on
the same runner pool) can leave .qwen/ files with restrictive
permissions (chmod -R a-w). The next job's checkout step then fails
with "error: unable to unlink old '.qwen/...': Permission denied"
because git cannot delete the read-only files during workspace
cleanup.
Add a pre-checkout step that chmods .qwen/ writable and removes it
before the checkout action runs. This is a no-op on fresh runners
where .qwen/ doesn't exist yet.
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: 易良 <1204183885@qq.com>