qwen-code/scripts/copy_bundle_assets.js
Shaojin Wen 10291e16f2
refactor(core): move review skill incident narratives to DESIGN.md (#8499)
* perf(review): issue independent setup calls in one response

Measured on a real small-PR run: the stretch from parse-args to the
first agent launch took 7 minutes of wall clock, one round-trip at a
time, on calls that never needed an order — pr-context, comment-status
and the Step 2 rules load are mutually independent reads. Step 1 now
tells the orchestrator to issue all three in a single response (the
same rule Step 3 already enforces for the agent fan-out) and to page
their outputs in shared responses too. comment-status loses its
wait-for-the-context-file guard in worktree mode: learning whether
inline comments exist cost a serial round-trip, while running it on a
commentless PR just writes an empty index. Step 6's two deterministic
gates (script-lint, test-plan) get the same one-response note.

The orderings that matter are kept explicit: fetch-pr before everything
(it creates the worktree and the plan), the roster after the rules load
(it bakes the rules into every brief).

* refactor(core): move review skill incident narratives to DESIGN.md

SKILL.md is injected wholesale into the review orchestrator's context on
every /review run and re-billed on each of its turns, and ~16KB of it was
incident narrative — accounts of past dogfood failures and measurements
that justify rules but are not themselves instructions.

Move 50 such narrative blocks into a new 'Measured incidents (moved from
SKILL.md)' section of DESIGN.md (47 anchors, not loaded at runtime),
leaving every rule in place with a short
'(measured; DESIGN.md — <anchor>)' pointer. Force-bearing figures stay
inline where the number is the argument (e.g. the ~161s cold npm ci, the
41% test-code median, the PR #6457 one-of-five checklist measurement).

No instruction, gate, format, flag, threshold, or ordering changed; the
YAML frontmatter and all 35 fenced code blocks are byte-identical, and
the MUST / Do not / never imperative counts are unchanged outside the
moved narrative text (verified by script).

SKILL.md: 237,847 -> 228,266 bytes; DESIGN.md: 106,708 -> 125,184 bytes.

* fix(review): keep DESIGN.md out of the runtime bundle and pin pointers

The slim refactor left DESIGN.md shipped beside SKILL.md in
dist/bundled/, so one curious read_file of the 125 KB maintainer
document would cost more context than the refactor saves. The bundle
copy now skips DESIGN.md, and SKILL.md gains a one-line guard telling
the orchestrator the pointers are for humans auditing a rule.

Also addresses review feedback: a test pins both directions of the
SKILL.md incident-pointer mapping, the transcribed-argument narrative
keeps its referent after the move, the incidents section title loses
its changelog suffix, and Step 2 no longer asks for a base fetch that
fetch-pr already performed.

* fix(review): gate setup batching by effort and consolidate incident blocks

Address round-1 review feedback on the skill-slim PR:

- Gate the ONE-response setup batch and the comment-status call to high
  and medium effort, matching Step 2's low-effort skip.
- Scope the Step 6 lint/test-plan batching to same-repo PR reviews.
- Merge same-run incident blocks (self-composed Approve into the
  paraphrased roster prompt; archive verdict into the narrated-away cap),
  cross-reference the roster-size and relocated-Critical tellings, and
  state the #8368 path in its block plus the pointer it was missing.
- Pointer-ize the last inline QQChannel narrative and fix the
  scripts-nobody-ran summary to match its block.
- Extend the DESIGN.md exclusion to copy_files.js so the transpiled
  dist/src build and the published core tarball stop shipping it.
- Pin the no-read_file guard and the batch ordering constraints in
  SKILL.test.ts, and fail loudly on pointers the regex cannot parse.

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-08-04 12:41:18 +00:00

205 lines
7.2 KiB
JavaScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { copyFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
import { dirname, join, basename, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { glob } from 'glob';
import fs from 'node:fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const defaultRoot = join(__dirname, '..');
const BUNDLED_SKILL_TEST_FILE_RE =
/\.(?:test|spec)\.(?:d\.)?[cm]?[jt]sx?(?:\.map)?$/;
export function copyBundleAssets({ root = defaultRoot } = {}) {
const distDir = join(root, 'dist');
const coreVendorDir = join(root, 'packages', 'core', 'vendor');
// Create the dist directory if it doesn't exist
if (!existsSync(distDir)) {
mkdirSync(distDir);
}
// Find and copy all .sb files from packages to the root of the dist directory
const sbFiles = glob.sync('packages/**/*.sb', { cwd: root });
for (const file of sbFiles) {
copyFileSync(join(root, file), join(distDir, basename(file)));
}
console.log('Copied sandbox profiles to dist/');
// Copy vendor directory (contains ripgrep binaries)
console.log('Copying vendor directory...');
if (existsSync(coreVendorDir)) {
const destVendorDir = join(distDir, 'vendor');
copyRecursiveSync(coreVendorDir, destVendorDir);
console.log('Copied vendor directory to dist/');
} else {
console.warn(`Warning: Vendor directory not found at ${coreVendorDir}`);
}
// Copy bundled skills (e.g. /review) so they are available at runtime.
// In the esbuild bundle, import.meta.url resolves to dist/cli.js, so
// SkillManager looks for bundled skills at dist/bundled/.
const bundledSkillsDir = join(
root,
'packages',
'core',
'src',
'skills',
'bundled',
);
if (existsSync(bundledSkillsDir)) {
const destBundledDir = join(distDir, 'bundled');
fs.rmSync(destBundledDir, { recursive: true, force: true });
copyRecursiveSync(bundledSkillsDir, destBundledDir, {
// DESIGN.md files are maintainer design narratives, not runtime inputs;
// shipping one would hand a review a ~125 KB read_file target that
// outweighs the context the slimmed skill saves.
skipEntry: (entry) =>
isBundledSkillTestFile(entry) || entry === 'DESIGN.md',
});
console.log('Copied bundled skills to dist/bundled/');
} else {
console.warn(
`Warning: Bundled skills directory not found at ${bundledSkillsDir}`,
);
}
// Copy user docs into qc-helper bundled skill so it can reference them at runtime.
// The qc-helper skill reads docs from a `docs/` subdirectory relative to its own
// directory. In the esbuild bundle this becomes dist/bundled/qc-helper/docs/.
const userDocsDir = join(root, 'docs', 'users');
if (existsSync(userDocsDir)) {
const destDocsDir = join(distDir, 'bundled', 'qc-helper', 'docs');
copyRecursiveSync(userDocsDir, destDocsDir);
console.log('Copied docs/users/ to dist/bundled/qc-helper/docs/');
} else {
console.warn(`Warning: User docs directory not found at ${userDocsDir}`);
}
// Copy builtin locales so bundled dist/cli.js can load UI translations at runtime.
// Published packages already include these via prepare-package.js; bundle output
// should mirror that behavior for local `node dist/cli.js` runs.
const localesDir = join(root, 'packages', 'cli', 'src', 'i18n', 'locales');
if (existsSync(localesDir)) {
const destLocalesDir = join(distDir, 'locales');
copyRecursiveSync(localesDir, destLocalesDir);
console.log('Copied builtin locales to dist/locales/');
} else {
console.warn(`Warning: Locales directory not found at ${localesDir}`);
}
// Copy extension templates so bundled dist/cli.js can scaffold
// `/extensions new` from the runtime examples directory.
const extensionExamplesDir = join(
root,
'packages',
'cli',
'src',
'commands',
'extensions',
'examples',
);
if (existsSync(extensionExamplesDir)) {
const destExtensionExamplesDir = join(distDir, 'examples');
copyRecursiveSync(extensionExamplesDir, destExtensionExamplesDir);
console.log('Copied extension examples to dist/examples/');
} else {
console.warn(
`Warning: Extension examples directory not found at ${extensionExamplesDir}`,
);
}
// Copy the built Web Shell SPA (index.html + assets/) so the bundled
// `qwen serve` can serve the browser UI at its root path. The library
// build outputs (dist/index.js, dist/types) are for npm consumers and are
// intentionally NOT copied. Source only exists after the web-shell
// workspace is built (npm run build); when absent (e.g. a --cli-only
// build, or bundling without a prior full build) we warn and skip so the
// bundle step never fails — the daemon then runs API-only at runtime.
const webShellDistDir = join(root, 'packages', 'web-shell', 'dist');
const webShellIndexHtml = join(webShellDistDir, 'index.html');
const webShellAssetsDir = join(webShellDistDir, 'assets');
if (existsSync(webShellIndexHtml) && existsSync(webShellAssetsDir)) {
const destWebShellDir = join(distDir, 'web-shell');
mkdirSync(destWebShellDir, { recursive: true });
copyFileSync(webShellIndexHtml, join(destWebShellDir, 'index.html'));
copyRecursiveSync(webShellAssetsDir, join(destWebShellDir, 'assets'));
console.log('Copied Web Shell UI to dist/web-shell/');
} else {
console.warn(
`Warning: Web Shell assets not found at ${webShellDistDir}; ` +
'dist/web-shell/ will be absent and `qwen serve` runs API-only. ' +
'Run a full `npm run build` before bundling to include the UI.',
);
}
console.log('\n✅ All bundle assets copied to dist/');
}
if (isDirectRun()) {
copyBundleAssets();
}
function isDirectRun() {
return process.argv[1]
? fileURLToPath(import.meta.url) === resolve(process.argv[1])
: false;
}
/**
* Recursively copy directory
*/
function copyRecursiveSync(src, dest, options = {}) {
if (!existsSync(src)) {
return;
}
const stats = statSync(src);
if (stats.isDirectory()) {
if (!existsSync(dest)) {
mkdirSync(dest, { recursive: true });
}
const entries = fs.readdirSync(src);
for (const entry of entries) {
if (entry === '.DS_Store' || options.skipEntry?.(entry)) {
continue;
}
const srcPath = join(src, entry);
const destPath = join(dest, entry);
copyRecursiveSync(srcPath, destPath, options);
}
} else {
copyFileSync(src, dest);
// Preserve execute permissions for binaries
const srcStats = statSync(src);
if (srcStats.mode & 0o111) {
fs.chmodSync(dest, srcStats.mode);
}
}
}
function isBundledSkillTestFile(fileName) {
return BUNDLED_SKILL_TEST_FILE_RE.test(fileName);
}