ci: make the Prettier lane a real gate (#11117)

* chore: format the 35 files CI had been silently rewriting

`scripts/lint.js --prettier` and the release workflow's `Format Project`
step both run `prettier --write`, which reformats in place and exits 0.
Neither is followed by a dirty-tree check, so the rewrites are discarded
when the job ends and these files have stayed unformatted on `main`.

This is the output of `prettier --write` over exactly those files, so the
next commit can turn the check into a real gate without failing on a
backlog. No behaviour change: every hunk is whitespace, quoting or
wrapping.

Verified with prettier 3.6.1, the version pinned in package-lock.json.
`prettier --check .` over the whole repo reports these 35 and nothing
else.

Refs #11109

* fix(ci): make the Prettier lane a real gate

`runPrettier()` ran `prettier --write .`. Prettier exits 0 after
reformatting, and no caller checks the tree afterwards, so the lane
passed on unformatted code and CI threw the rewrites away — the 35 files
in the previous commit are what it had been silently fixing and
discarding.

ESLint does not cover the gap: eslint.config.js loads
eslint-config-prettier, which disables formatting rules rather than
enforcing them. With this change the only enforcement point outside the
husky pre-commit hook actually enforces something.

`npm run lint:all` now reports unformatted files instead of fixing them;
`npm run format` is the fix. Prettier's own failure message says so.

Refs #11109

* fix(release): drop Format Project, it writes and discards

The step ran `npm run format` (`prettier --write .`) and nothing in
release.yml inspected the tree afterwards — no `git diff --exit-code`, no
`git status --porcelain` anywhere in the file. The rewrites died with the
job, so the only failure it could produce was Prettier crashing on an
unparsable file, which the Run Lint step reports anyway.

It cost 13m58s on a quiet host and 20m45s on a contended one, against a
30-minute cap that `quality_static` hit twice today (runs 33957952281 and
33963757913, both cancelled mid-lint with nothing failing). The remaining
steps are `npm ci` (1m44s) and `lint:ci` (~8m20s), so the job now has real
headroom instead of finishing 24 seconds over the wall.

Formatting is now gated where it belongs: the previous commit makes the
PR-CI Prettier lane `--check`.

Refs #11109

* fix(ci): keep prettier --check out of .qwen/ and the pre-paint theme script

Two things the `--write` → `--check` flip broke that `--write` had hidden.

`.qwen/` is gitignored as `.qwen/*` and holds machine-written state —
pending-skills/, auto-skill-*/, learned-skill-*/,
computer-use/installed.json. Prettier does not read .gitignore, so it was
walking in and silently reformatting whatever a contributor had there;
under `--check` that turns into a failed `npm run lint:all` on files git
does not even track. CI never saw it (fresh checkout, tracked files only),
which is exactly why it went unnoticed. Adding `.qwen/` to
.prettierignore also drops the one tracked file under it —
skills/find-simplifications/SKILL.md, force-added past .gitignore — from
the previous formatting commit, since a directory the gate should not
police should not be reformatted for it either.

The inline theme script in web-shell's index.html is restored to its
original single line behind `<!-- prettier-ignore -->`. It is a
render-blocking script in <head> that applies the stored theme before
first paint; the expanded form ships ~600 more bytes on the critical path
and buys no readability the comment above it does not already give. A
change about CI hygiene should not be reshaping that.

Refs #11109

* fix(ci): keep prettier --check out of vendored packages/mobile-mcp

eslint.config.js has ignored `packages/mobile-mcp/**` since it landed —
"vendored mobile-next/mobile-mcp; has own eslint config" — and
.prettierignore already carries its sibling `packages/cua-driver/`, listed
one line above with the same rationale. mobile-mcp was simply missed.

`--write` hid that: it reformatted the vendored package's own eslint
config to our house style every run and threw the result away. The
formatting commit in this PR would have made that permanent, widening the
diff against upstream on the next sync for a package this repo has already
decided not to lint. That file is reverted here and the directory is
ignored.

Same class as the .qwen/ fix in the previous commit: the gate reaching
somewhere the repo had already decided it should not.

Refs #11109

* fix(ci): mirror eslint's remaining global ignores in .prettierignore

`--check` makes .prettierignore load-bearing in a way `--write` never was:
under `--write` a file prettier should not touch was reformatted and the
result discarded, so nobody noticed; under `--check` it fails the lane.

These three are the rest of eslint.config.js's global ignores, added for
the reasons it already states. `scripts/codemod/fixtures/` is the one that
matters: those files are "intentionally non-idiomatic ink input/output",
so the obvious response to a `--check` failure there — reformat to house
style — would corrupt the fixtures the codemod tests assert against. The
trap is not live today (all three currently satisfy prettier), which is
precisely why it is worth closing before someone meets it.

Refs #11109

* chore: format converter.test.ts, which drifted onto main since this branch

This branch merged main 32 commits after its base and `prettier --check .`
found one new offender. It is the same gate failing the same way: nothing
enforces formatting, so `--write` reformatted this file on every CI run
since it landed and threw the result away.

Worth noting as evidence rather than noise — the backlog this PR fixes was
not a one-off from before someone got careless; it accrues continuously,
and it accrued again while the PR was open.

Refs #11109

* Revert "fix(release): drop Format Project, it writes and discards"

This reverts the release.yml half of this PR. #11195 removes that step on
its own, in one file and four lines, and two PRs racing on the same
deletion helps nobody.

The reasoning and the evidence are unchanged and now live on #11195: the
dry-run release dispatched from this branch with the step removed
(34022919438) went 18/18 green in 65m43s, quality_static finished in
7m00s against main's 8m57s, and Run Lint was 4m52s vs 4m43s — nothing
downstream ever read what the step produced.

Dropping release.yml from this PR also widens who can review it. It was
the only file here owned by @pomelo-nwu and @wenshao alone; what remains
under CODEOWNERS is /packages/core/, whose owner list is six people. The
Prettier gate no longer waits on the two maintainers who must sign off on
release workflow changes.

Refs #11109, #11195

* fix(ci): make the gate resolve the file set npm run format resolves

Two review findings, one root cause: the gate was checking a different set
of files than the tree it is supposed to police.

**The `.qwen/` ignore rested on a false premise (R1-1, @chiga0).** Prettier
3.6's `--ignore-path` already defaults to `[.gitignore, .prettierignore]`,
so every machine-written path that entry's comment named —
`pending-skills/`, `auto-skill-*/`, `learned-skill-*/` — was outside the
gate before it via `.gitignore:32`. What the entry actually excluded is the
corpus `.gitignore` deliberately re-includes: `.qwen/commands/**`,
`skills/**`, `agents/**`, `team-memory/**`, `review-context.json` — 57
tracked, hand-authored files. The gate then passed because the surface had
been narrowed, not because the tree was formatted, and contributors editing
those docs could ship them unformatted with both enforcement points silent.
The entry is dropped and the first commit's formatting of
`skills/find-simplifications/SKILL.md`, reverted alongside it, is re-landed.

**The gate and the remedy used different CLIs (R1-2).** `runPrettier()` ran
the classic `prettier --check .` while `npm run format` runs
`prettier --experimental-cli --write .`. The classic CLI reads only the
repo-root `.gitignore`; the experimental one also honours nested ones, and
this repo has 23 of those. Verified on a scratch tree: given
`pkg/.gitignore` containing `lib/`, classic reports `pkg/lib/nested.js` and
experimental skips it. So the lane could go red on a locally built package's
output that `npm run format` provably cannot clear. The gate now uses the
same CLI as the remedy it names.

Refs #11109

* fix(ci): let the pre-commit hook cover what the gate now checks, and pin it

**The hook was narrower than the gate (R1-5).** `lint-staged` formatted only
`*.{js,jsx,ts,tsx}` and `*.{json,md}`, but the gate checks every extension
Prettier has a parser for. Of the 35 files the first commit formatted, 29
(83%) are in classes the hook never touched — 13 `.yml`, 12 `.mjs`, 3
`.css`, 1 `.html`. Left alone, the backlog this PR just cleared would
re-accumulate, and now as a red CI step on someone else's PR rather than as
silent drift. `.mjs`, `.cjs`, `.yml`, `.yaml`, `.css` and `.html` join the
Prettier-only group; ESLint's globs are unchanged, since the gap is about
formatting coverage, not lint coverage.

**Nothing pinned the flag (R1-4).** `scripts/tests/lint.test.js` exercised
only the linter-directory helpers, and `ci-platform-lanes.test.js` asserts
that the `Run Prettier` step exists and is profile-gated, not what it runs.
A refactor, or a well-meant "make lint auto-fix again", could restore the
silent pass with every suite green. Two assertions now pin it: that
`runPrettier()` checks rather than writes, and that it uses the same
Prettier CLI as `npm run format`, so the two cannot drift apart again the
way R1-2 found them.

Refs #11109

* docs: correct the instructions and comments the --check flip falsified

**Three tracked instructions to automation agents now say the opposite of
what the code does (R1-9), and one is fed to a CI lane today** —
`.github/workflows/qwen-triage.yml` re-pins `verify-pr` from the base commit
and hands it to the verifying agent.

- `verify-pr/SKILL.md` told agents never to run `scripts/lint.js` with no
  arguments because it would rewrite the working tree under their A/B
  harness. That is no longer why. The prohibition is kept — it is still
  right — but re-grounded in what the no-arg form actually does now
  (`setupLinters()` wipes the linter temp dir and re-downloads three pinned
  binaries), and `--prettier` joins the enumerated non-mutating checks it
  was the only flag missing from.
- `find-simplifications/SKILL.md` and its `references/land.md` told the
  simplification agent that a formatting diff "carries zero signal" because
  CI runs `--write`. An agent believing that would skip `npm run format`
  after a deletion changed line shape — on the very extensions the hook does
  not cover — and go red on formatting it was told could not matter.
- `update-ecs-runner-qwen-workflow.test.js`'s comment justified parsing two
  matrix layouts by "CI runs `prettier --write .` before this suite". CI no
  longer rewrites anything; the two layouts are still worth parsing, so the
  comment is re-grounded in the checked-in form versus older branches.

**The index.html rationale overstated its own number (R1-8, @chiga0).** It
claimed the expanded script ships "~600 more bytes"; measured against this
repo's Prettier 3.6.1 and its `tabWidth: 2`, it is +258 source and +8
gzipped here, +287/+20 by the reviewer's method — either way roughly half
what was claimed, and "~600" is what the same expansion costs at
`--tab-width 4`. Since the prose ships verbatim inside the built
`index.html`, the fix is to drop the contested figure and keep the comment
short. The `<!-- prettier-ignore -->` guard itself stays: it is load-bearing.

Refs #11109

* chore: format classify-profile.test.mjs, drifted again after commit 1

Third instance in this PR's lifetime, and the sharpest: commit `b1b94a8762`
formatted this exact file as one of the 35, and `main` reintroduced
unformatted content in it within a day. The other two were
`converter.test.ts` at the first merge and this file at the second.

The gate is the fix; until it lands, every merge from `main` re-opens the
same hole.

Refs #11109
This commit is contained in:
易良 2026-09-07 00:44:01 +00:00 • committed by GitHub
parent 7567824d4c
commit 5b470befa0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 261 additions and 138 deletions

View file

@ -1,5 +1,5 @@
name: 'Configure Windows self-hosted runner'
description: 'Tunes a self-hosted Windows runner for the test gates: exports the Linux gates'' C.UTF-8 locale env (inert on Windows, where Node collates through ICU), redirects TEMP/TMP to RUNNER_TEMP, and puts Git Bash on PATH for bash-shell steps. Runs after actions/checkout because repository-local actions resolve from the workspace; callers turn off autocrlf before the checkout itself. Shared by the Windows merge-queue gate and the runner smoke workflow so both always run an identical configuration.'
description: "Tunes a self-hosted Windows runner for the test gates: exports the Linux gates' C.UTF-8 locale env (inert on Windows, where Node collates through ICU), redirects TEMP/TMP to RUNNER_TEMP, and puts Git Bash on PATH for bash-shell steps. Runs after actions/checkout because repository-local actions resolve from the workspace; callers turn off autocrlf before the checkout itself. Shared by the Windows merge-queue gate and the runner smoke workflow so both always run an identical configuration."
runs:
using: 'composite'

View file

@ -30,7 +30,10 @@ test('MDX is executable content, never docs_only', () => {
assert.equal(classifyChangedFiles(['docs/guide.mdx']), 'full');
assert.equal(classifyChangedFiles(['docs/guide.MDX']), 'full');
assert.equal(classifyChangedFiles(['README.mdx']), 'full');
assert.equal(classifyChangedFiles(['docs/usage.md', 'docs/guide.mdx']), 'full');
assert.equal(
classifyChangedFiles(['docs/usage.md', 'docs/guide.mdx']),
'full',
);
});
test('falls back to full for root docs names used as directories', () => {
@ -56,7 +59,9 @@ test('keeps yaml-dependent helper suites on the full profile', () => {
// dependency-free github_ci_only lane can never execute it; downgrading a
// PR that only edits that suite would green-light it without running it.
assert.equal(
classifyChangedFiles(['.github/scripts/web-shell-visuals-publish.test.mjs']),
classifyChangedFiles([
'.github/scripts/web-shell-visuals-publish.test.mjs',
]),
'full',
);
});

View file

@ -191,9 +191,8 @@ test('the body stays bounded on a total-suite failure', () => {
`body is ${body.length} chars, must stay under GitHub's 65,536 limit`,
);
assert.ok(body.includes(`- …and ${400 - MAX_BODY_TESTS} more`));
const markerCount = (
body.match(new RegExp(TEST_MARKER_PREFIX, 'g')) ?? []
).length;
const markerCount = (body.match(new RegExp(TEST_MARKER_PREFIX, 'g')) ?? [])
.length;
assert.ok(
markerCount <= MAX_SEARCH_MARKERS,
`body carries ${markerCount} markers, at most ${MAX_SEARCH_MARKERS}`,

View file

@ -240,7 +240,7 @@ describe('release note classification', () => {
// labels — its projectCards lookup fails on affected gh builds).
"if (args[0] === 'api' && args[1] === '-X' && (args[2] === 'POST' || args[2] === 'DELETE') && /\\/issues\\/\\d+\\/labels/.test(args[3])) {",
" const action = args[2] === 'DELETE' ? 'remove' : 'add';",
" const number = args[3].match(/\\/issues\\/(\\d+)\\/labels/)[1];",
' const number = args[3].match(/\\/issues\\/(\\d+)\\/labels/)[1];',
` process.getBuiltinModule('node:fs').appendFileSync(${JSON.stringify(updates)}, number + ' ' + action + '\\n');`,
' process.exit(0);',
'}',

View file

@ -7,12 +7,7 @@ import path from 'node:path';
const options = parseArguments(process.argv.slice(2));
const assets = fs.readdirSync(options.assets).sort();
const patterns = {
macos: [
/-arm64\.zip$/i,
/-x64\.zip$/i,
/-arm64\.dmg$/i,
/-x64\.dmg$/i,
],
macos: [/-arm64\.zip$/i, /-x64\.zip$/i, /-arm64\.dmg$/i, /-x64\.dmg$/i],
windows: [/-setup\.exe$/i],
linux: [/\.AppImage$/i],
};
@ -20,7 +15,9 @@ const selectedPatterns = patterns[options.platform];
if (!selectedPatterns) {
throw new Error(`Invalid --platform: ${options.platform}`);
}
const names = selectedPatterns.map((pattern) => selectArtifact(assets, pattern));
const names = selectedPatterns.map((pattern) =>
selectArtifact(assets, pattern),
);
const artifacts = names.map((name) => readArtifact(assets, name));
const primary = artifacts[0];

View file

@ -1,13 +1,22 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import {
mkdtempSync,
mkdirSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { after, before, describe, it } from 'node:test';
import { fileURLToPath } from 'node:url';
const root = mkdtempSync(join(tmpdir(), 'dsw-tb-manifest-'));
const script = join(dirname(fileURLToPath(import.meta.url)), 'make-terminal-bench-manifest.py');
const script = join(
dirname(fileURLToPath(import.meta.url)),
'make-terminal-bench-manifest.py',
);
let archive;
before(() => {
@ -23,18 +32,30 @@ before(() => {
writeFileSync(join(task, 'instruction.md'), 'test\n');
}
archive = join(root, 'tasks.tar.gz');
const result = spawnSync('tar', ['-czf', archive, '-C', root, 'tasks'], { encoding: 'utf8' });
const result = spawnSync('tar', ['-czf', archive, '-C', root, 'tasks'], {
encoding: 'utf8',
});
assert.equal(result.status, 0, result.stderr);
});
after(() => rmSync(root, { recursive: true, force: true }));
const run = (...args) => spawnSync('python3', [script, '--archive', archive, ...args], { encoding: 'utf8' });
const run = (...args) =>
spawnSync('python3', [script, '--archive', archive, ...args], {
encoding: 'utf8',
});
describe('make-terminal-bench-manifest', () => {
it('selects one exact task for an end-to-end smoke', () => {
const output = join(root, 'one.json');
const result = run('--limit', '1', '--instance-id', 'task-42', '--output', output);
const result = run(
'--limit',
'1',
'--instance-id',
'task-42',
'--output',
output,
);
assert.equal(result.status, 0, result.stderr);
const manifest = JSON.parse(readFileSync(output, 'utf8'));
assert.equal(manifest.expected_instances, 1);
@ -51,7 +72,14 @@ describe('make-terminal-bench-manifest', () => {
});
it('rejects an unknown exact task', () => {
const result = run('--limit', '1', '--instance-id', 'missing', '--output', join(root, 'bad.json'));
const result = run(
'--limit',
'1',
'--instance-id',
'missing',
'--output',
join(root, 'bad.json'),
);
assert.equal(result.status, 1);
assert.match(result.stderr, /Unknown Terminal-Bench/);
});

View file

@ -61,9 +61,9 @@ const updateJobName =
workflow.match(/\n {2}update:\n {4}name: '([^']*)'/)?.[1] ?? '';
const poolPrefix = updateJobName.replace(/\$\{\{.*\}\}$/, '');
// Both layouts, because CI runs `prettier --write .` before this suite and
// prettier reflows the inline matrix array onto its own lines. A parser that
// reads only the checked-in layout passes locally and finds zero pools there.
// Both layouts, because the checked-in workflow carries the reflowed form
// prettier produces for an inline matrix array, and older branches still
// carry the single-line one. A parser that reads only one finds zero pools.
function parsePools(text) {
return (text.match(/\n\s+runner:\s*\[([^\]]*)\]/)?.[1] ?? '')
.split(',')

View file

@ -50,7 +50,7 @@ jobs:
runner: 'windows-2022'
arch: 'x64'
steps:
- uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
- uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
- uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
with:
node-version: '22'

View file

@ -46,7 +46,7 @@ jobs:
run: 'echo "push_image=${PUSH_IMAGE}" >> "$GITHUB_OUTPUT"'
- name: 'Checkout repository'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
with:
ref: '${{ github.ref }}'
@ -189,7 +189,7 @@ jobs:
issues: 'write'
steps:
- name: 'Checkout repository'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
with:
persist-credentials: false

View file

@ -30,7 +30,7 @@ jobs:
timeout-minutes: 30
steps:
- name: 'Checkout'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
- name: 'Initialize CodeQL'
uses: 'github/codeql-action/init@df559355d593797519d70b90fc8edd5db049e7a2' # ratchet:github/codeql-action/init@v3

View file

@ -24,7 +24,7 @@ jobs:
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
- name: 'Setup Pages'
uses: 'actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d' # ratchet:actions/configure-pages@v6

View file

@ -74,7 +74,7 @@ jobs:
id: 'gate'
env:
EVENT_NAME: '${{ github.event_name }}'
RELEASE_TAG: '${{ github.event.release.tag_name || '''' }}'
RELEASE_TAG: "${{ github.event.release.tag_name || '' }}"
RELEASE_PRERELEASE: '${{ github.event.release.prerelease || false }}'
INPUT_EXECUTION_BACKEND: '${{ inputs.execution_backend }}'
INPUT_INSTANCE_LIMIT: '${{ inputs.instance_limit }}'
@ -136,12 +136,12 @@ jobs:
benchmark:
name: 'Dispatch SWE-bench and Terminal-Bench to DSW'
needs: 'release_gate'
if: '${{ needs.release_gate.outputs.should_run == ''true'' }}'
if: "${{ needs.release_gate.outputs.should_run == 'true' }}"
runs-on: ['self-hosted', 'Linux', 'X64', 'qwen-benchmark-dsw-hk-eas']
timeout-minutes: 15
env:
RELEASE_TAG: '${{ github.event_name == ''release'' && github.event.release.tag_name || inputs.release_tag }}'
QWEN_REF: '${{ github.event_name == ''release'' && github.event.release.tag_name || inputs.qwen_release_tag || inputs.release_tag }}'
RELEASE_TAG: "${{ github.event_name == 'release' && github.event.release.tag_name || inputs.release_tag }}"
QWEN_REF: "${{ github.event_name == 'release' && github.event.release.tag_name || inputs.qwen_release_tag || inputs.release_tag }}"
INSTANCE_LIMIT: '${{ needs.release_gate.outputs.instance_limit }}'
BENCHMARK_INSTANCE_ID: '${{ needs.release_gate.outputs.instance_id }}'
TERMINAL_BENCH_LIMIT: '${{ needs.release_gate.outputs.terminal_bench_limit }}'

View file

@ -3809,7 +3809,6 @@ jobs:
gh pr comment "$PR_NUMBER" --repo "$REPO" --body-file "${WORKDIR}/report.md" ||
echo "::warning::Resolve was skipped, but posting the skip-reason comment failed."
# Publishes what the agent job produced — verification, push and the result
# comment — on a runner that never executed the agent. It starts from a
# fresh checkout, fetches the base and head refs from GitHub itself, and

View file

@ -25,7 +25,7 @@ jobs:
decision: '${{ steps.assess.outputs.decision }}'
steps:
- name: 'Checkout trusted precheck script'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
with:
ref: '${{ github.event.repository.default_branch }}'
sparse-checkout: '.github/scripts/pr-safety-precheck.mjs'

View file

@ -34,7 +34,7 @@ on:
default: false
concurrency:
group: '${{ github.workflow }}-${{ github.event.inputs.create_nightly_release == ''true'' && ''nightly'' || github.event.inputs.create_preview_release == ''true'' && ''preview'' || ''stable'' }}'
group: "${{ github.workflow }}-${{ github.event.inputs.create_nightly_release == 'true' && 'nightly' || github.event.inputs.create_preview_release == 'true' && 'preview' || 'stable' }}"
cancel-in-progress: false
jobs:
@ -100,7 +100,7 @@ jobs:
fi
- name: 'Checkout'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
with:
ref: '${{ github.event.inputs.ref || github.sha }}'
fetch-depth: 0
@ -136,7 +136,7 @@ jobs:
echo "sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}"
- name: 'Setup Node.js'
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
@ -203,7 +203,7 @@ jobs:
MANUAL_VERSION: '${{ inputs.version }}'
- name: 'Setup Python'
uses: 'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405' # v6.2.0
uses: 'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405' # v6.2.0
with:
# Keep in sync with packages/sdk-python/pyproject.toml [project] requires-python.
python-version: '3.11'

View file

@ -64,7 +64,7 @@ jobs:
steps:
- name: 'Checkout'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
with:
ref: '${{ github.event.release.tag_name || github.event.inputs.ref || github.sha }}'
fetch-depth: 0
@ -208,7 +208,7 @@ jobs:
steps:
- name: 'Checkout'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
with:
ref: '${{ github.event.release.tag_name || github.event.inputs.ref || github.sha }}'
fetch-depth: 0
@ -295,7 +295,7 @@ jobs:
steps:
- name: 'Checkout'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
with:
ref: '${{ github.event.release.tag_name || github.event.inputs.ref || github.sha }}'

View file

@ -77,10 +77,10 @@ jobs:
python-version: ['3.10', '3.11', '3.12']
steps:
- name: 'Checkout'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
- name: 'Set up Python'
uses: 'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405' # v6.2.0
uses: 'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405' # v6.2.0
with:
python-version: '${{ matrix.python-version }}'

View file

@ -24,7 +24,7 @@ jobs:
group: '${{ github.workflow }}-stale'
cancel-in-progress: true
steps:
- uses: 'actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899' # v10.3.0
- uses: 'actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899' # v10.3.0
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
# Issues are intentionally disabled here; a separate policy will

View file

@ -75,7 +75,14 @@ jobs:
if: "${{ github.repository == 'QwenLM/qwen-code' }}"
strategy:
matrix:
runner: ['ecs-update-hk-1', 'ecs-update-hk-2', 'ecs-update-hk-3', 'ecs-update-hk-4', 'ecs-update-hk-5']
runner:
[
'ecs-update-hk-1',
'ecs-update-hk-2',
'ecs-update-hk-3',
'ecs-update-hk-4',
'ecs-update-hk-5',
]
fail-fast: false
runs-on: ['self-hosted', 'linux', 'x64', '${{ matrix.runner }}']
concurrency:
@ -150,7 +157,7 @@ jobs:
issues: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
with:
persist-credentials: false

View file

@ -23,3 +23,15 @@ Thumbs.db
packages/vscode-ide-companion/schemas/settings.schema.json
packages/cli/src/services/insight/templates/insightTemplate.ts
packages/cua-driver/
# The rest of eslint.config.js's global ignores, for the same reasons it
# gives. scripts/codemod/fixtures is the load-bearing one: those files are
# "intentionally non-idiomatic ink input/output", so normalising them to
# house style would corrupt the very data the codemod tests assert on.
scripts/codemod/fixtures/
packages/live-host/
packages/desktop-shell/runtime/
# Vendored from mobile-next/mobile-mcp and carries its own eslint config, so
# reformatting it to house style only widens the diff against upstream on the
# next sync. eslint.config.js already ignores packages/mobile-mcp/** for the
# same reason; .prettierignore had cua-driver but not its sibling.
packages/mobile-mcp/

View file

@ -78,26 +78,26 @@ path this repo ships or loads. A path named by more than one row takes the
most restrictive outcome: Never-a-target beats Report-only, which beats
Landable.
| Territory | Outcome |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `packages/cli/src` — the whole package (`generated/` stays under the Never-a-target row below; `**/*.sb` stays under the Report-only row below; `i18n/locales/**` and `commands/extensions/examples/**` stay under the Report-only row below; `**/*.test.ts(x)`, `**/*.spec.ts(x)`, `**/__snapshots__/**` are never targets, always searched as consumers) | Landable |
| `scripts/`, `esbuild.config.js`, `eslint.legacy-filenames.mjs`, root manifests | Landable |
| Territory | Outcome |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `packages/cli/src` — the whole package (`generated/` stays under the Never-a-target row below; `**/*.sb` stays under the Report-only row below; `i18n/locales/**` and `commands/extensions/examples/**` stay under the Report-only row below; `**/*.test.ts(x)`, `**/*.spec.ts(x)`, `**/__snapshots__/**` are never targets, always searched as consumers) | Landable |
| `scripts/`, `esbuild.config.js`, `eslint.legacy-filenames.mjs`, root manifests | Landable |
| Whole files or directories nothing consumes by any mechanism named above — no import and no runtime read, loader, manifest, or tool config — anywhere outside `packages/core/src`, `packages/audio-capture`, `packages/channels`, `packages/sdk-*`, `packages/acp-bridge`, `packages/vscode-ide-companion`, `packages/chrome-extension`, `packages/zed-extension`, `packages/web-shell`, `packages/core/vendor`, `.github`, and the Never-a-target row below | Landable |
| `docs/users/**`, `docs/developers/**`, `docs/index.md`, `docs/_meta.ts`, `packages/cli/src/i18n/locales/**`, `packages/cli/src/commands/extensions/examples/**` — as whole files or directories | **Report-only** — copied into the published tarball (`scripts/prepare-package.js` copies the locales and extension examples; `scripts/copy_bundle_assets.js` copies `docs/users/` for qc-helper) and consumed by the published docs site (`docs-site/scripts/link-public-docs.mjs` symlinks `docs/users/` and `docs/developers/` into the Nextra build per `PUBLIC_DOC_ROOTS` and copies `docs/index.md` and `docs/_meta.ts`; the site discovers pages by walking that tree); consumers are runtime reads — qc-helper's doc paths, the i18n loader's segment-assembled `import()`, `/extensions new` scaffolds — never imports. Individual orphan locale keys stay class-4 candidates: their proof greps the literal key, naming its mechanism |
| `docs-site/` | **Report-only** — standalone published-site app, not a workspace member; route files are consumed by Next.js filesystem routing and an out-of-repo deploy, never imports, and no in-repo CI builds it |
| Tracked `.qwen/skills/**`, `.qwen/agents/**`, `.qwen/e2e-tests/**`, `docs/design/**`, `docs/plans/**` | **Report-only** — consumed by the skill loader, agent definitions, and process readers (including `AGENTS.md` itself), never imports |
| `AGENTS.md`, `CLAUDE.md`, `SECURITY.md`, `CONTRIBUTING.md`, `.prettierrc.json`, `.prettierignore`, `.editorconfig`, `.nvmrc`, `.npmrc`, `.yamllint.yml` | **Report-only** — consumed by external tooling through filename convention (agent harnesses, GitHub's security-policy UI, prettier and yamllint config auto-discovery, nvm, editors); never imports, and an in-repo grep for them measures only prose |
| Anything under `packages/core/src` | **Report-only** — published |
| `packages/audio-capture`, `packages/channels` | **Report-only** — npm-published (`--access public`) |
| `packages/core/vendor/**`, `packages/web-shell` | **Report-only** — shipped inside the published `@qwen-code/qwen-code` tarball / served to browsers by `qwen serve`; consumers are bundled or browser-side, never imports |
| `packages/cli/src/utils/**/*.sb` | **Report-only** — copied into the published bundle by extension glob (`scripts/copy_bundle_assets.js` copies `packages/**/*.sb`, `scripts/prepare-package.js` lists `'*.sb'`) and read at runtime through a segment-assembled path (`resolveSeatbeltProfileFile()` builds `sandbox-macos-${profile}.sb`); consumers are never imports, and a basename grep measures zero |
| Any key in `packages/cli/src/config/settingsSchema.ts` | **Report-only** — see below |
| `packages/sdk-*`, `packages/acp-bridge`, protocol/wire shapes | **Report-only** — out-of-repo consumers |
| `packages/vscode-ide-companion`, `packages/chrome-extension`, `packages/zed-extension` | **Report-only** — shipped as store manifests; the store is the consumer |
| `.github/` — workflows, actions, CODEOWNERS | **Report-only** — consumed GitHub-side: triggers, required checks, cross-repo `uses:` |
| `package.json` dependencies | **Report-only** — bundlers and postinstall hide consumers |
| Comments, JSDoc, commented-out code | **Out of scope** — `AGENTS.md` says do not delete existing comments as cleanup |
| `packages/desktop-shell`, `packages/cua-driver`, `packages/mobile-mcp`, `**/generated/**`, `**/*.test.ts(x)`, `**/*.spec.ts(x)`, `**/__snapshots__/**` | Never a target; always searched as consumers — vitest discovers tests by filename glob, nothing imports them, so an import-based orphan detector matches every live test vacuously |
| `docs/users/**`, `docs/developers/**`, `docs/index.md`, `docs/_meta.ts`, `packages/cli/src/i18n/locales/**`, `packages/cli/src/commands/extensions/examples/**` — as whole files or directories | **Report-only** — copied into the published tarball (`scripts/prepare-package.js` copies the locales and extension examples; `scripts/copy_bundle_assets.js` copies `docs/users/` for qc-helper) and consumed by the published docs site (`docs-site/scripts/link-public-docs.mjs` symlinks `docs/users/` and `docs/developers/` into the Nextra build per `PUBLIC_DOC_ROOTS` and copies `docs/index.md` and `docs/_meta.ts`; the site discovers pages by walking that tree); consumers are runtime reads — qc-helper's doc paths, the i18n loader's segment-assembled `import()`, `/extensions new` scaffolds — never imports. Individual orphan locale keys stay class-4 candidates: their proof greps the literal key, naming its mechanism |
| `docs-site/` | **Report-only** — standalone published-site app, not a workspace member; route files are consumed by Next.js filesystem routing and an out-of-repo deploy, never imports, and no in-repo CI builds it |
| Tracked `.qwen/skills/**`, `.qwen/agents/**`, `.qwen/e2e-tests/**`, `docs/design/**`, `docs/plans/**` | **Report-only** — consumed by the skill loader, agent definitions, and process readers (including `AGENTS.md` itself), never imports |
| `AGENTS.md`, `CLAUDE.md`, `SECURITY.md`, `CONTRIBUTING.md`, `.prettierrc.json`, `.prettierignore`, `.editorconfig`, `.nvmrc`, `.npmrc`, `.yamllint.yml` | **Report-only** — consumed by external tooling through filename convention (agent harnesses, GitHub's security-policy UI, prettier and yamllint config auto-discovery, nvm, editors); never imports, and an in-repo grep for them measures only prose |
| Anything under `packages/core/src` | **Report-only** — published |
| `packages/audio-capture`, `packages/channels` | **Report-only** — npm-published (`--access public`) |
| `packages/core/vendor/**`, `packages/web-shell` | **Report-only** — shipped inside the published `@qwen-code/qwen-code` tarball / served to browsers by `qwen serve`; consumers are bundled or browser-side, never imports |
| `packages/cli/src/utils/**/*.sb` | **Report-only** — copied into the published bundle by extension glob (`scripts/copy_bundle_assets.js` copies `packages/**/*.sb`, `scripts/prepare-package.js` lists `'*.sb'`) and read at runtime through a segment-assembled path (`resolveSeatbeltProfileFile()` builds `sandbox-macos-${profile}.sb`); consumers are never imports, and a basename grep measures zero |
| Any key in `packages/cli/src/config/settingsSchema.ts` | **Report-only** — see below |
| `packages/sdk-*`, `packages/acp-bridge`, protocol/wire shapes | **Report-only** — out-of-repo consumers |
| `packages/vscode-ide-companion`, `packages/chrome-extension`, `packages/zed-extension` | **Report-only** — shipped as store manifests; the store is the consumer |
| `.github/` — workflows, actions, CODEOWNERS | **Report-only** — consumed GitHub-side: triggers, required checks, cross-repo `uses:` |
| `package.json` dependencies | **Report-only** — bundlers and postinstall hide consumers |
| Comments, JSDoc, commented-out code | **Out of scope** — `AGENTS.md` says do not delete existing comments as cleanup |
| `packages/desktop-shell`, `packages/cua-driver`, `packages/mobile-mcp`, `**/generated/**`, `**/*.test.ts(x)`, `**/*.spec.ts(x)`, `**/__snapshots__/**` | Never a target; always searched as consumers — vitest discovers tests by filename glob, nothing imports them, so an import-based orphan detector matches every live test vacuously |
A settings key with zero read sites is still not cleanup. The key is likely
documented under `docs/users/` and completed from the generated schema;
@ -331,9 +331,9 @@ easy surface is gone — the remaining work is design, not sweeping).
delete a colleague's. Removing the doc comment attached to a symbol you are
deleting in the same commit is not comment deletion; anything else is out of
scope.
- No formatting sweeps, no dependency bumps, no drive-by renames. CI's
Prettier step runs `--write`, not `--check`, so a formatting diff carries
zero signal.
- No formatting sweeps, no dependency bumps, no drive-by renames. But CI's
Prettier step runs `--check` and exits non-zero, so any deletion that
changes line shape must be accompanied by `npm run format`.
- Never file a candidate whose evidence is a line count, a complexity score,
or "this looks complex." It names no consumer and proves no deletion.

View file

@ -139,9 +139,8 @@ Green CI is weaker evidence than it looks for a deletion:
skips most steps. Deleting docs alongside code can quietly buy you less CI
than you think.
- The Prettier CI step (the `Run Prettier` step in `ci.yml` → `runPrettier()`
in `scripts/lint.js`) runs `prettier --write .`, not `--check`, so
formatting never fails a build — and a formatting-only diff carries zero
signal.
in `scripts/lint.js`) runs `prettier --check .`, so unformatted code fails
the build. Run `npm run format` before pushing.
- The pre-commit hook runs formatters over staged files. Re-read the diff
after committing; if the hook reformatted lines you did not otherwise touch,
restore them in a follow-up commit (history is additive here — never amend).

View file

@ -743,10 +743,10 @@ since the merge-base, say so and re-measure there.
`bash -n` and `shellcheck` on extracted `run:` blocks always work; the
repo's wrapper only lints when the pinned binaries are present, so
install them with `node scripts/lint.js --setup` and then invoke the
individual non-mutating checks (`--actionlint`, `--yamllint`, `--eslint`).
**Never run `node scripts/lint.js` with no arguments** — the no-arg form
also runs `prettier --write .`, which rewrites the PR working tree
underneath your A/B and replay harnesses. If the tools cannot be installed
individual non-mutating checks (`--actionlint`, `--yamllint`, `--eslint`,
`--prettier`). **Never run `node scripts/lint.js` with no arguments** — the
no-arg form calls `setupLinters()`, which wipes the linter temp dir and
re-downloads three pinned binaries before running anything. If the tools cannot be installed
in-container, say which gate you could not run rather than implying it
passed. For a new automated trigger, do the day-one cost math
— arrival rate against the job's drain rate. Event history needs the API,

View file

@ -190,7 +190,7 @@
"prettier --write",
"eslint --fix --max-warnings 0 --no-warn-ignored"
],
"*.{json,md}": [
"*.{mjs,cjs,json,md,yml,yaml,css,html}": [
"prettier --write"
]
}

View file

@ -8,11 +8,7 @@ import type { CommandModule } from 'yargs';
import { getErrorMessage } from '../../utils/errors.js';
import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js';
import { extensionToOutputString, getExtensionManager } from './utils.js';
import {
t,
initializeI18n,
resolveLanguageSetting,
} from '../../i18n/index.js';
import { t, initializeI18n, resolveLanguageSetting } from '../../i18n/index.js';
import { loadSettings } from '../../config/settings.js';
export async function handleList() {

View file

@ -311,7 +311,7 @@ export async function initializeI18n(
export function resolveLanguageSetting(
settingsLanguage?: string,
): SupportedLanguage | 'auto' {
return (
process.env['QWEN_CODE_LANG'] || settingsLanguage || 'auto'
) as SupportedLanguage | 'auto';
return (process.env['QWEN_CODE_LANG'] || settingsLanguage || 'auto') as
| SupportedLanguage
| 'auto';
}

View file

@ -54,10 +54,16 @@ export function ScopeSelectStep({
const title =
mode === 'disable'
? t('Disable "{{name}}" - Select Scope', {
name: getExtensionDisplayName(selectedExtension, getCurrentLanguage()),
name: getExtensionDisplayName(
selectedExtension,
getCurrentLanguage(),
),
})
: t('Enable "{{name}}" - Select Scope', {
name: getExtensionDisplayName(selectedExtension, getCurrentLanguage()),
name: getExtensionDisplayName(
selectedExtension,
getCurrentLanguage(),
),
});
return (

View file

@ -60,7 +60,10 @@ export function UninstallConfirmStep({
<Box flexDirection="column" gap={1}>
<Text color={theme.status.error}>
{t('Are you sure you want to uninstall extension "{{name}}"?', {
name: getExtensionDisplayName(selectedExtension, getCurrentLanguage()),
name: getExtensionDisplayName(
selectedExtension,
getCurrentLanguage(),
),
})}
</Text>
<Text color={theme.status.error}>

View file

@ -6045,9 +6045,7 @@ describe('OpenAIContentConverter', () => {
};
const tools = [
{
functionDeclarations: [
{ name: 'stable', parametersJsonSchema },
],
functionDeclarations: [{ name: 'stable', parametersJsonSchema }],
},
] as Tool[];
const compileStrict = vi.spyOn(SchemaValidator, 'compileStrict');

View file

@ -53,9 +53,10 @@ class InMemoryServerTransport {
}
/** Build a canned client-hosted MCP server exposing one echo tool. */
function buildCannedServer(
sink: (message: JSONRPCMessage) => void,
): { transport: InMemoryServerTransport; ready: Promise<void> } {
function buildCannedServer(sink: (message: JSONRPCMessage) => void): {
transport: InMemoryServerTransport;
ready: Promise<void>;
} {
const server = new McpServer({
name: 'chrome-tools',
version: '0.0.1',

View file

@ -32,7 +32,9 @@ const kernelChildren = () => {
let failures = 0;
const check = (label, cond, detail) => {
console.log(`${cond ? 'PASS' : 'FAIL'} ${label}${detail ? ` — ${detail}` : ''}`);
console.log(
`${cond ? 'PASS' : 'FAIL'} ${label}${detail ? ` — ${detail}` : ''}`,
);
if (!cond) failures++;
};
@ -86,8 +88,14 @@ const stillAlive = before.filter((pid) => {
return false;
}
});
check('kernel child reaped', stillAlive.length === 0, `alive=${stillAlive.join(',') || 'none'}`);
check(
'kernel child reaped',
stillAlive.length === 0,
`alive=${stillAlive.join(',') || 'none'}`,
);
if (!exited) server.kill('SIGKILL');
console.log(failures === 0 ? '\nLIFECYCLE SMOKE PASSED' : `\n${failures} FAILED`);
console.log(
failures === 0 ? '\nLIFECYCLE SMOKE PASSED' : `\n${failures} FAILED`,
);
process.exit(failures === 0 ? 0 : 1);

View file

@ -29,7 +29,9 @@ const run = async (code) => {
let failures = 0;
const check = (label, cond, detail) => {
console.log(`${cond ? 'PASS' : 'FAIL'} ${label}${detail ? ` — ${detail}` : ''}`);
console.log(
`${cond ? 'PASS' : 'FAIL'} ${label}${detail ? ` — ${detail}` : ''}`,
);
if (!cond) failures++;
};
@ -46,7 +48,11 @@ try {
);
const r3 = await run('nodeRepl.write(typeof host);');
const text3 = r3.mcp.content.find((b) => b.type === 'text')?.text ?? '';
check('dynamic import binding persists', text3.includes('string'), text3.trim());
check(
'dynamic import binding persists',
text3.includes('string'),
text3.trim(),
);
// 3. Image output (1x1 PNG).
const png =
@ -75,5 +81,9 @@ try {
manager.dispose();
}
console.log(failures === 0 ? '\nALL SMOKE CHECKS PASSED' : `\n${failures} CHECK(S) FAILED`);
console.log(
failures === 0
? '\nALL SMOKE CHECKS PASSED'
: `\n${failures} CHECK(S) FAILED`,
);
process.exit(failures === 0 ? 0 : 1);

View file

@ -839,10 +839,7 @@ function readSuccessfulBindings(module, bindingExports) {
async function handleExec(message) {
if (!config || !loader) throw new Error('kernel is not initialized');
if (activeExec) throw new Error('kernel received overlapping executions');
if (
pendingCancelExecId !== null &&
pendingCancelExecId !== message.execId
) {
if (pendingCancelExecId !== null && pendingCancelExecId !== message.execId) {
pendingCancelExecId = null;
}
const expected = sortedBindingDescriptors();

View file

@ -49,7 +49,11 @@ new AgentSideConnection(
authMethods: [{ id: 'openai', name: 'Use OpenAI API key' }],
agentCapabilities: {
loadSession: false,
promptCapabilities: { image: true, audio: false, embeddedContext: false },
promptCapabilities: {
image: true,
audio: false,
embeddedContext: false,
},
},
};
},
@ -85,7 +89,11 @@ new AgentSideConnection(
title: text.slice(0, 80),
},
options: [
{ optionId: 'proceed_once', name: 'Allow once', kind: 'allow_once' },
{
optionId: 'proceed_once',
name: 'Allow once',
kind: 'allow_once',
},
{ optionId: 'cancel', name: 'Cancel', kind: 'reject_once' },
],
});

View file

@ -250,18 +250,14 @@
overflow: hidden;
}
:global(
[data-web-shell-toolbar-popover][data-web-shell-compact-overlay]
) {
:global([data-web-shell-toolbar-popover][data-web-shell-compact-overlay]) {
width: min(360px, calc(100vw - 16px));
max-width: calc(100vw - 16px);
box-sizing: border-box;
padding: 6px;
}
:global(
[data-web-shell-toolbar-popover][data-web-shell-compact-overlay]
)
:global([data-web-shell-toolbar-popover][data-web-shell-compact-overlay])
.dropdownItemDesc {
display: -webkit-box;
overflow-wrap: anywhere;
@ -1329,10 +1325,7 @@
border-radius: 6px;
}
.editorShell[data-web-shell-compact-composer]
.toolbarRight
button
svg {
.editorShell[data-web-shell-compact-composer] .toolbarRight button svg {
width: 16px;
height: 16px;
}

View file

@ -15,8 +15,7 @@
padding: 3px 9px;
border-radius: 20px;
cursor: pointer;
border: 1px solid
color-mix(in srgb, var(--git-mode-current) 20%, transparent);
border: 1px solid color-mix(in srgb, var(--git-mode-current) 20%, transparent);
background: color-mix(in srgb, var(--git-mode-current) 10%, transparent);
color: var(--git-mode-current);
transition:

View file

@ -262,11 +262,6 @@
gap: 8px;
}
/* ── branch select (searchable) ─────────────── */
.branchSelectTrigger {
display: inline-flex;

View file

@ -24,8 +24,11 @@
<html> background matches the app theme on mobile. Without this, the
iOS Safari rubber-band overscroll and the status-bar / home-indicator
safe areas briefly show the default white canvas. Storage key mirrors
THEME_STORAGE_KEY in main.tsx.
THEME_STORAGE_KEY in main.tsx. Kept on one line behind the guard below:
this comment ships verbatim in the built index.html, so the rationale
stays short.
-->
<!-- prettier-ignore -->
<script>
!function(){try{var k='qwen-code-web-shell-theme',p=new URLSearchParams(location.search),t=p.get('theme');if(t!=='dark'&&t!=='light'){try{t=localStorage.getItem(k)}catch(_){t=null}}if(t!=='dark'&&t!=='light')t='dark';document.documentElement.classList.add('theme-'+t);var m=document.querySelector('meta[name=theme-color]');if(m)m.setAttribute('content',t==='light'?'#ffffff':'#0d0d0d')}catch(e){console.warn('theme-init:',e)}}();
</script>

View file

@ -26,16 +26,25 @@ const require = createRequire(import.meta.url);
const { Agent } = require('../packages/core/node_modules/undici/index.js');
const ITERATIONS = parseInt(process.env['ITERATIONS'] ?? '3', 10);
const REQUEST_TIMEOUT_MS = parseInt(process.env['REQUEST_TIMEOUT_MS'] ?? '5000', 10);
const REQUEST_TIMEOUT_MS = parseInt(
process.env['REQUEST_TIMEOUT_MS'] ?? '5000',
10,
);
const DEFAULT_ENDPOINTS = [
{ url: 'https://api.openai.com', label: 'OpenAI' },
{ url: 'https://api.anthropic.com', label: 'Anthropic' },
{ url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', label: 'DashScope (openai-compatible)' },
{ url: 'https://api.openai.com', label: 'OpenAI' },
{ url: 'https://api.anthropic.com', label: 'Anthropic' },
{
url: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
label: 'DashScope (openai-compatible)',
},
];
const extraUrls = process.env['BENCHMARK_URLS']
? process.env['BENCHMARK_URLS'].split(' ').filter(Boolean).map((url) => ({ url, label: url }))
? process.env['BENCHMARK_URLS']
.split(' ')
.filter(Boolean)
.map((url) => ({ url, label: url }))
: [];
const ENDPOINTS = [...DEFAULT_ENDPOINTS, ...extraUrls];
@ -146,10 +155,10 @@ for (const endpoint of ENDPOINTS) {
console.log('\n\n=== Results ===\n');
console.log(
'Endpoint'.padEnd(36) +
'Cold (avg)'.padStart(12) +
'Warm (avg)'.padStart(12) +
'Saved'.padStart(10) +
'Improvement'.padStart(13),
'Cold (avg)'.padStart(12) +
'Warm (avg)'.padStart(12) +
'Saved'.padStart(10) +
'Improvement'.padStart(13),
);
console.log('─'.repeat(83));
@ -157,10 +166,10 @@ for (const r of results) {
const status = r.pct >= 30 ? '✓' : r.pct >= 10 ? '~' : '✗';
console.log(
r.label.slice(0, 35).padEnd(36) +
fmt(r.avgCold).padStart(12) +
fmt(r.avgWarm).padStart(12) +
fmt(r.saved).padStart(10) +
`${r.pct.toFixed(1)}% ${status}`.padStart(13),
fmt(r.avgCold).padStart(12) +
fmt(r.avgWarm).padStart(12) +
fmt(r.saved).padStart(10) +
`${r.pct.toFixed(1)}% ${status}`.padStart(13),
);
}

View file

@ -296,9 +296,17 @@ export function runYamllint() {
}
}
// `--check`, not `--write`. `--write` reformats in place and exits 0
// whether or not anything changed, and no caller inspects the tree
// afterwards, so for the life of this script the Prettier lane reported a
// pass on unformatted code and CI discarded the rewrites when the job
// ended. ESLint does not cover the gap either: eslint.config.js loads
// eslint-config-prettier, which turns formatting rules off rather than
// enforcing them. `npm run format` remains the way to fix what this
// reports.
export function runPrettier() {
console.log('\nRunning Prettier...');
if (!runCommand('prettier --write .')) {
if (!runCommand('prettier --experimental-cli --check .')) {
process.exit(1);
}
}

View file

@ -110,7 +110,9 @@ function renderDelta(current, baseline) {
const b = baseline.counts[p.name];
const d = c - b;
const arrow = d < 0 ? '↓' : d > 0 ? '↑' : '·';
stdout.write(` ${p.name.padEnd(18)} ${d > 0 ? '+' : ''}${d} ${arrow}\n`);
stdout.write(
` ${p.name.padEnd(18)} ${d > 0 ? '+' : ''}${d} ${arrow}\n`,
);
}
stdout.write(
'\ntip: lower clearTerminalPair (and lower clearScreen) on "current" wins.\n',

View file

@ -17,6 +17,7 @@ import {
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
// getLinterTempDir joins with the platform separator; compare normalized
@ -223,3 +224,43 @@ describe('linter directories', () => {
15_000,
);
});
// The --write to --check flip in runPrettier() is the whole point of the
// Prettier lane: --write reformats in place and exits 0 whether or not
// anything changed, so the lane reported a pass on unformatted code for as
// long as it was there (#11109). Nothing else pins the flag —
// ci-platform-lanes.test.js asserts the step exists and is gated, not what it
// runs — so a refactor or a well-meant "make lint auto-fix again" could
// restore the silent pass with every suite green.
describe('prettier lane', () => {
const repoRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..',
);
const lintSource = readFileSync(
path.join(repoRoot, 'scripts', 'lint.js'),
'utf8',
);
it('checks formatting rather than rewriting it', () => {
expect(lintSource).toContain('prettier --experimental-cli --check .');
expect(lintSource).not.toMatch(/prettier[^\n'"]*--write/);
});
// The gate and the remedy it points people at must resolve the same file
// set. The classic CLI reads only the repo-root .gitignore while the
// experimental one also honours nested ones, and this repo has 23 of those,
// so a mismatch lets the lane go red on files `npm run format` cannot reach.
it('uses the same Prettier CLI as npm run format', () => {
const pkg = JSON.parse(
readFileSync(path.join(repoRoot, 'package.json'), 'utf8'),
);
const formatIsExperimental =
pkg.scripts.format.includes('--experimental-cli');
const gateIsExperimental = lintSource.includes(
'prettier --experimental-cli --check',
);
expect(gateIsExperimental).toBe(formatIsExperimental);
});
});