diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4b439af1..b11341f5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,6 +167,11 @@ jobs: - name: Integration tests (headless Node) run: node --test "crates/bashkit-wasm/__test__/*.test.mjs" + - name: Browser compatibility and transfer-size gates + run: | + node crates/bashkit-wasm/bench/compat.mjs + node crates/bashkit-wasm/bench/artifact.mjs crates/bashkit-wasm/pkg --check + audit: name: Audit runs-on: ubuntu-latest diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml index a178a4945..0c0fd99f7 100644 --- a/.github/workflows/publish-wasm.yml +++ b/.github/workflows/publish-wasm.yml @@ -72,6 +72,11 @@ jobs: - name: Integration tests (headless Node) run: node --test "crates/bashkit-wasm/__test__/*.test.mjs" + - name: Browser compatibility and transfer-size gates + run: | + node crates/bashkit-wasm/bench/compat.mjs + node crates/bashkit-wasm/bench/artifact.mjs crates/bashkit-wasm/pkg --check + - name: Upload pkg uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: diff --git a/Cargo.toml b/Cargo.toml index 2aad79c80..a93c54942 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -209,6 +209,14 @@ codegen-units = 1 panic = "unwind" strip = "symbols" +# Browser cold start is transfer-bound. Keep native release throughput settings; +# use size-oriented LLVM optimization and whole-program LTO only for the JS-host +# WASM package. Retain release's panic policy and all interpreter safety checks. +[profile.wasm-release] +inherits = "release" +opt-level = "s" +lto = "fat" + # Important decision: separate `bench` profile uses fat LTO for the most # aggressive cross-crate inlining. Criterion benches run rarely and we'd # rather spend extra compile time once than miss inlining opportunities diff --git a/crates/bashkit-wasm/README.md b/crates/bashkit-wasm/README.md index 9a16cba49..eb16047e7 100644 --- a/crates/bashkit-wasm/README.md +++ b/crates/bashkit-wasm/README.md @@ -294,3 +294,29 @@ just build-wasm ## License MIT, part of the [Bashkit](https://github.com/everruns/bashkit) project. + +## Deployment and cold start + +Serve `bashkit_wasm_bg.wasm` as `application/wasm` with Brotli or gzip HTTP +compression. Raw npm file size is not transfer size. Use content-hashed asset +URLs with long-lived immutable caching; keep the JS glue and WASM from the same +build together. `initBashkit()` uses wasm-bindgen's streaming loader when the +server sends the correct MIME type. A slow mobile connection can take more than +15 seconds to download a multi-megabyte bundle, so choose startup deadlines from +your application's measured network conditions. + +The package includes `build-info.json`: source commit, dirty-checkout status, +compiler/tools, Cargo profile/features, and the SHA-256 of the shipped WASM. +Source archives without Git metadata record a null source identity. A source +checkout between releases can have the same package version as an older npm +release; compare the source commit as well as the version. + +Maintainers can run the bounded compatibility and compressed-size gates: + +```sh +node crates/bashkit-wasm/bench/compat.mjs +node crates/bashkit-wasm/bench/artifact.mjs crates/bashkit-wasm/pkg --check +``` + +The [browser benchmark methodology](https://github.com/everruns/bashkit/blob/main/crates/bashkit-wasm/bench/README.md) describes reproducible +cold-start and execution experiments and the measured profile tradeoff. diff --git a/crates/bashkit-wasm/__test__/artifact.test.mjs b/crates/bashkit-wasm/__test__/artifact.test.mjs new file mode 100644 index 000000000..693977ae1 --- /dev/null +++ b/crates/bashkit-wasm/__test__/artifact.test.mjs @@ -0,0 +1,33 @@ +// A tiny valid WASM fixture keeps CLI/budget regression coverage bounded. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { spawnSync } from "node:child_process"; + +test("artifact CLI accepts --check with default package and enforces budgets", () => { + const root = mkdtempSync(join(tmpdir(), "bashkit-artifact-")); + try { + const bench = join(root, "bench"), pkg = join(root, "pkg"); + mkdirSync(bench); mkdirSync(pkg); + const script = join(bench, "artifact.mjs"); + copyFileSync(new URL("../bench/artifact.mjs", import.meta.url), script); + writeFileSync(join(pkg, "index.js"), "export {};"); + writeFileSync(join(pkg, "bashkit_wasm.js"), "export {};"); + writeFileSync(join(pkg, "bashkit_wasm_bg.wasm"), Buffer.from([0, 97, 115, 109, 1, 0, 0, 0])); + const budget = join(bench, "size-budget.json"); + writeFileSync(budget, '{"raw":1000,"gzip":1000,"brotli":1000}'); + const run = args => spawnSync(process.execPath, [script, ...args], { encoding: "utf8", timeout: 10000 }); + const result = run(["--check"]); + assert.equal(result.status, 0, result.stderr); + assert.equal(JSON.parse(result.stdout).total.raw, 28); + const explicit = run([pkg, "--check"]); + assert.equal(explicit.status, 0, explicit.stderr); + assert.deepEqual(JSON.parse(explicit.stdout).total, JSON.parse(result.stdout).total); + writeFileSync(budget, '{"raw":27}'); + const rejected = run(["--check"]); + assert.notEqual(rejected.status, 0); + assert.match(rejected.stderr, /28 exceeds 27 byte budget/); + } finally { rmSync(root, { recursive: true, force: true }); } +}); diff --git a/crates/bashkit-wasm/__test__/package.test.mjs b/crates/bashkit-wasm/__test__/package.test.mjs new file mode 100644 index 000000000..c3d22900e --- /dev/null +++ b/crates/bashkit-wasm/__test__/package.test.mjs @@ -0,0 +1,33 @@ +// Exercise the archive's public contract, not just an unpacked build directory. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { execFileSync } from "node:child_process"; +import { Terminal } from "../pkg/index.js"; +const pkg = new URL("../pkg/", import.meta.url); +const read = name => readFileSync(new URL(name, pkg)); + +test("package records verifiable source and artifact identity", () => { + const info = JSON.parse(read("build-info.json")); + if (info.sourceRevision === null) { + assert.equal(info.sourceDirty, null); + assert.notEqual(process.env.GITHUB_ACTIONS, "true", "CI/release must record Git identity"); + } else { + assert.match(info.sourceRevision, /^[0-9a-f]{40}$/); + assert.equal(typeof info.sourceDirty, "boolean"); + assert.equal(info.sourceRevision, execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim()); + } + assert.equal(info.version, JSON.parse(read("package.json")).version); + assert.equal(info.wasmSha256, createHash("sha256").update(read("bashkit_wasm_bg.wasm")).digest("hex")); + assert.equal(info.profile, "wasm-release"); + assert.equal(info.features.includes("terminal"), Terminal !== undefined); +}); + +test("npm archive contains every runtime file and build identity", () => { + const [archive] = JSON.parse(execFileSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], { + cwd: pkg, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 30000, + })); + const files = new Set(archive.files.map(file => file.path)); + for (const name of ["index.js", "index.d.ts", "bashkit_wasm.js", "bashkit_wasm.d.ts", "bashkit_wasm_bg.wasm", "build-info.json"]) assert.ok(files.has(name), `archive lacks ${name}`); +}); diff --git a/crates/bashkit-wasm/bench/README.md b/crates/bashkit-wasm/bench/README.md new file mode 100644 index 000000000..a15c587b1 --- /dev/null +++ b/crates/bashkit-wasm/bench/README.md @@ -0,0 +1,47 @@ +# Browser package experiments + +These experiments complement the full package integration suite. Run from the +repository root after `just build-wasm`. They require GNU Bash 5+ on `PATH` (on +macOS, `/bin/bash` 3.2 is unsuitable) and Node 22+. + +- `node crates/bashkit-wasm/bench/compat.mjs [pkg ...]`: 24 deterministic, + self-contained shell cases against a fresh GNU Bash oracle and fresh Bashkit + instances. Exact stdout bytes and status; stderr is not scored. Worker lifetime + is capped at 30 seconds; each oracle process at two seconds. One package is a + pass/fail CI gate; multiple packages produce a comparison without failing for + an old release's known gaps. This is a regression sample, not a population + compatibility percentage. No corpus or package is fetched by the gate. +- `node crates/bashkit-wasm/bench/artifact.mjs [pkg] [--check]`: raw, gzip-9, + Brotli-11 bytes for the actual JS + WASM load graph, SHA-256 identities, and + WASM code/data/other sections. `--check` applies `size-budget.json`, with about + 4-5% headroom above the selected shipping profile. Review new measurements + before deliberately raising a budget. No wall-clock threshold in CI. +- `node crates/bashkit-wasm/bench/runtime.mjs pkg`: arithmetic, substitution, + jq, and text pipelines; one warmup and seven measured fresh-instance runs. + Output is checked for status and consistency. Compare on the same host, + without competing builds. Times from a busy development host are exploratory. +- `TRIALS=5 node crates/bashkit-wasm/bench/cold-start.mjs pkg-before pkg-after`: + Chromium, Brotli-11 server, fresh HTTP cache per trial, rotated package order, + loopback plus CDP Fast 4G (1,012,500 B/s, 165 ms) and Slow 4G (180,000 B/s, + 562.5 ms), page CPU throttled 4x. Requires an existing Playwright installation + and Chrome; `PLAYWRIGHT_MODULE` can name its `index.mjs` and `CHROME_CHANNEL` + selects a browser channel. Trials are limited to 1..20. Every visit must + initialize the real package, execute `echo hi`, drive `Terminal` when present, + and paint output over two animation frames. Each navigation/wait is capped at + 60 seconds; browser contexts and the loopback server close on completion/error. + +The cold-start page intentionally has no framework, renderer, shared terminal +WASM, or worker dependency. It measures the package, not brush-web's complete +navigation-to-prompt path. All execution is on the page; CDP CPU throttling +therefore covers this experiment's interpreter (unlike a worker experiment). +Fresh contexts guarantee cold HTTP caches, but browser-process compiled-code +caches may be shared. Network measurements remain representative; do not infer +physical-phone compile time. The JSON records every resource and milestone, +Chromium/Node versions, bytes, hashes, and raw samples. + +Redirect JSON output to retain a run. Historical investigation results are in +`results/`; these specialized package experiments are linked here rather than +mixed with the native bashkit-bench or Criterion result schemas. + +See also: [package guide](../README.md), +[browser design](../../../knowledge/runtimes/browser-package.md). diff --git a/crates/bashkit-wasm/bench/artifact.mjs b/crates/bashkit-wasm/bench/artifact.mjs new file mode 100644 index 000000000..593997361 --- /dev/null +++ b/crates/bashkit-wasm/bench/artifact.mjs @@ -0,0 +1,42 @@ +// Transfer bytes, not raw bytes alone, determine cold-start network cost. +import { readFileSync, existsSync, realpathSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { createHash } from "node:crypto"; +import { gzipSync, brotliCompressSync, constants } from "node:zlib"; + +export function artifact(pkg) { + const files = ["index.js", "bashkit_wasm.js", "bashkit_wasm_bg.wasm"].map(name => { + const bytes = readFileSync(resolve(pkg, name)); + return { name, sha256: createHash("sha256").update(bytes).digest("hex"), raw: bytes.length, + gzip: gzipSync(bytes, { level: 9 }).length, + brotli: brotliCompressSync(bytes, { params: { [constants.BROTLI_PARAM_QUALITY]: 11 } }).length }; + }); + const bytes = readFileSync(resolve(pkg, "bashkit_wasm_bg.wasm")); + let offset = 8; + function leb() { let n = 0, shift = 0, b; do { b = bytes[offset++]; n += (b & 127) * 2 ** shift; shift += 7; } while (b & 128); return n; } + const sections = []; + while (offset < bytes.length) { + const id = bytes[offset++], length = leb(), start = offset; + let name = ({ 10: "code", 11: "data" })[id] ?? `section-${id}`; + if (id === 0) { const n = leb(); name = `custom:${bytes.subarray(offset, offset + n).toString()}`; } + sections.push({ name, raw: length }); + offset = start + length; + } + return { files, total: Object.fromEntries(["raw", "gzip", "brotli"].map(k => [k, files.reduce((n, f) => n + f[k], 0)])), sections }; +} +// Node canonicalizes the module URL, but argv can retain a symlink (e.g. /var +// versus /private/var on macOS). Imports must never run the CLI accidentally. +if (process.argv[1] && existsSync(process.argv[1]) && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) { + const paths = process.argv.slice(2).filter(arg => arg !== "--check"); + if (paths.length > 1) throw new Error("expected one package directory"); + const pkg = resolve(paths[0] ?? new URL("../pkg/", import.meta.url).pathname); + const report = artifact(pkg); + console.log(JSON.stringify(report, null, 2)); + if (process.argv.includes("--check")) { + const budgets = JSON.parse(readFileSync(new URL("./size-budget.json", import.meta.url))); + for (const [kind, budget] of Object.entries(budgets)) { + if (report.total[kind] > budget) throw new Error(`${kind}: ${report.total[kind]} exceeds ${budget} byte budget`); + } + } +} diff --git a/crates/bashkit-wasm/bench/cold-start.mjs b/crates/bashkit-wasm/bench/cold-start.mjs new file mode 100644 index 000000000..d749e49c8 --- /dev/null +++ b/crates/bashkit-wasm/bench/cold-start.mjs @@ -0,0 +1,85 @@ +// Manual browser experiment. CI gates deterministic bytes/semantics, not noisy wall time. +// Supply PLAYWRIGHT_MODULE when Playwright is installed outside this workspace. +import { createServer } from "node:http"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { brotliCompressSync, constants } from "node:zlib"; +import { artifact } from "./artifact.mjs"; + +const { chromium } = await import(process.env.PLAYWRIGHT_MODULE ?? "playwright"); +const packages = process.argv.slice(2).map(resolvePath => resolve(resolvePath)); +if (!packages.length) throw new Error("pass package directories to compare"); +const trials = Number(process.env.TRIALS ?? 5); +if (!Number.isInteger(trials) || trials < 1 || trials > 20) throw new Error("TRIALS must be 1..20"); +const conditions = { + local: null, + fast4g: { downloadThroughput: 1012500, uploadThroughput: 168750, latency: 165 }, + slow4g: { downloadThroughput: 180000, uploadThroughput: 84375, latency: 562.5 }, +}; +const routes = new Map(); +packages.forEach((pkg, i) => { + for (const file of ["index.js", "bashkit_wasm.js", "bashkit_wasm_bg.wasm"]) { + routes.set(`/${i}/${file}`, { type: file.endsWith("wasm") ? "application/wasm" : "text/javascript", + bytes: brotliCompressSync(readFileSync(resolve(pkg, file)), { params: { [constants.BROTLI_PARAM_QUALITY]: 11 } }) }); + } + const html = `
`;
+  routes.set(`/${i}/`, { type: "text/html", bytes: Buffer.from(html), plain: true });
+});
+const server = createServer((req, res) => {
+  const route = routes.get(req.url);
+  if (!route) { res.writeHead(404); res.end(); return; }
+  res.writeHead(200, { "Content-Type": route.type, "Content-Length": route.bytes.length,
+    "Cache-Control": "public, max-age=31536000, immutable", "Vary": "Accept-Encoding",
+    ...(route.plain ? {} : { "Content-Encoding": "br" }) });
+  res.end(route.bytes);
+});
+await new Promise(r => server.listen(0, "127.0.0.1", r));
+let browser;
+try {
+  browser = await chromium.launch({ channel: process.env.CHROME_CHANNEL ?? "chrome", headless: true });
+  const samples = [];
+  for (const [condition, throttle] of Object.entries(conditions)) {
+    for (let trial = 0; trial < trials; trial++) {
+      const order = packages.map((_, i) => (i + trial) % packages.length);
+      if (trial % 2) order.reverse();
+      for (const i of order) {
+        const context = await browser.newContext(); // Cold HTTP cache per sample.
+        try {
+          const page = await context.newPage();
+          const session = await context.newCDPSession(page);
+          await session.send("Network.enable");
+          if (throttle) await session.send("Network.emulateNetworkConditions", { offline: false, ...throttle });
+          await session.send("Emulation.setCPUThrottlingRate", { rate: 4 });
+          const errors = []; page.on("pageerror", e => errors.push(String(e)));
+          await page.goto(`http://127.0.0.1:${server.address().port}/${i}/`, { waitUntil: "domcontentloaded", timeout: 60000 });
+          await page.waitForFunction(() => window.result, null, { timeout: 60000 });
+          const result = await page.evaluate(() => window.result);
+          if (errors.length) throw new Error(errors.join("\n"));
+          samples.push({ package: i, condition, trial, ...result });
+          console.error(`${condition} ${trial + 1}/${trials} package ${i}: ${result.marks.prompt.toFixed(1)}ms`);
+        } finally { await context.close(); }
+      }
+    }
+  }
+  console.log(JSON.stringify({ method: "Cold context, Brotli 11, CDP Fast/Slow 4G, page CPU 4x; main-thread JS-host WASM, no terminal renderer dependency; prompt after first echo and two animation frames", browser: browser.version(), node: process.version,
+    packages: packages.map(pkg => ({ path: pkg, ...artifact(pkg) })), samples }, null, 2));
+} finally { await browser?.close(); await new Promise(r => server.close(r)); }
diff --git a/crates/bashkit-wasm/bench/compat-cases.json b/crates/bashkit-wasm/bench/compat-cases.json
new file mode 100644
index 000000000..490982c34
--- /dev/null
+++ b/crates/bashkit-wasm/bench/compat-cases.json
@@ -0,0 +1,26 @@
+[
+  {"name":"quoted array", "script":"a=('one two' three); printf '<%s>\\n' \"${a[@]}\""},
+  {"name":"sparse array", "script":"a=([2]=two [7]=seven); printf '%s\\n' \"${!a[@]}\" \"${#a[@]}\""},
+  {"name":"array slice", "script":"a=(a b c d e); printf '%s\\n' \"${a[@]: -2}\""},
+  {"name":"associative array", "script":"declare -A a=([key]='two words'); printf '%s\\n' \"${a[key]}\""},
+  {"name":"nameref assignment", "script":"a=old; declare -n r=a; r=new; echo \"$a $r\""},
+  {"name":"nameref array", "script":"a=(x y z); declare -n r=a; echo \"${r[1]} ${#r[@]}\""},
+  {"name":"case conversion", "script":"a='hEllo World'; echo \"${a^^} ${a,,}\""},
+  {"name":"pattern replacement", "script":"a=abcabc; echo \"${a//a/X} ${a#*b} ${a%%b*}\""},
+  {"name":"indirect expansion", "script":"a=hello; r=a; echo \"${!r}\""},
+  {"name":"dynamic scope", "script":"a=outer; g() { echo \"$a\"; }; f() { local a=inner; g; }; f; echo \"$a\""},
+  {"name":"arithmetic array", "script":"a=(4 5); ((a[1]+=3)); echo \"${a[1]}\""},
+  {"name":"brace sequence", "script":"printf '%s\\n' {03..09..3}"},
+  {"name":"command substitution", "script":"a=$(printf 'x\\n\\n'); printf '<%s>\\n' \"$a\""},
+  {"name":"subshell isolation", "script":"a=parent; (a=child; echo \"$a\"); echo \"$a\""},
+  {"name":"process substitution", "script":"read -r a < <(echo hello); echo \"$a\""},
+  {"name":"here string", "script":"read -r a b <<< 'one two'; echo \"$b $a\""},
+  {"name":"pipefail", "script":"set -o pipefail; false | true; echo $?"},
+  {"name":"errexit exception", "script":"set -e; false && echo no; echo yes"},
+  {"name":"case fallthrough", "script":"case x in x) echo a ;;& y) echo no ;; *) echo b ;; esac"},
+  {"name":"regex capture", "script":"[[ abc123 =~ ([a-z]+)([0-9]+) ]]; echo \"${BASH_REMATCH[1]} ${BASH_REMATCH[2]}\""},
+  {"name":"mapfile", "script":"mapfile -t a <<< $'one\\ntwo'; printf '<%s>\\n' \"${a[@]}\""},
+  {"name":"child shell", "script":"bash -c 'echo child'"},
+  {"name":"read delimiter", "script":"IFS=: read -r a b <<< 'one:two'; echo \"$a/$b\""},
+  {"name":"negative status", "script":"false"}
+]
diff --git a/crates/bashkit-wasm/bench/compat.mjs b/crates/bashkit-wasm/bench/compat.mjs
new file mode 100644
index 000000000..7f9171889
--- /dev/null
+++ b/crates/bashkit-wasm/bench/compat.mjs
@@ -0,0 +1,54 @@
+// Fresh instances, exact bytes/status, bounded worker lifetime. GNU Bash is the oracle.
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { pathToFileURL } from "node:url";
+import { execFileSync, spawnSync } from "node:child_process";
+import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
+
+if (!isMainThread) {
+  const { Bash, initBashkit } = await import(pathToFileURL(resolve(workerData.pkg, "index.js")));
+  await initBashkit(readFileSync(resolve(workerData.pkg, "bashkit_wasm_bg.wasm")));
+  for (const item of workerData.cases) {
+    const bash = new Bash({ profile: "interactive", maxCommands: 100000,
+      maxLoopIterations: 10000, maxMemory: 8 * 1024 * 1024, timeoutMs: 1000 });
+    try {
+      const result = await bash.execute(item.script);
+      parentPort.postMessage({ name: item.name, stdout: Buffer.from(result.stdoutBytes).toString("base64"), status: result.exitCode });
+      result.free();
+    } catch (error) {
+      parentPort.postMessage({ name: item.name, error: String(error) });
+    } finally { bash.free(); }
+  }
+} else {
+  const cases = JSON.parse(readFileSync(new URL("./compat-cases.json", import.meta.url)));
+  const oracle = process.env.BASH ?? "bash";
+  const version = execFileSync(oracle, ["--version"], { encoding: "utf8" }).split("\n")[0];
+  if (!/version ([5-9]|[1-9][0-9])\./.test(version)) throw new Error("GNU Bash 5+ required");
+  const expected = cases.map(item => {
+    const r = spawnSync(oracle, ["--noprofile", "--norc", "-c", item.script], { timeout: 2000, maxBuffer: 65536 });
+    if (r.error || r.signal) throw new Error(`oracle failed: ${item.name}`);
+    return { stdout: r.stdout.toString("base64"), status: r.status };
+  });
+  const packages = process.argv.slice(2);
+  if (!packages.length) packages.push(new URL("../pkg/", import.meta.url).pathname);
+  const reports = [];
+  for (const pkg of packages) {
+    const actual = await new Promise((resolveResults, reject) => {
+      const results = [];
+      const worker = new Worker(new URL(import.meta.url), { workerData: { pkg: resolve(pkg), cases } });
+      const timer = setTimeout(() => { worker.terminate(); reject(new Error("compatibility worker exceeded 30s")); }, 30000);
+      worker.on("message", item => results.push(item));
+      worker.on("error", reject);
+      worker.on("exit", code => { clearTimeout(timer); code ? reject(new Error(`worker exited ${code}`)) : resolveResults(results); });
+    });
+    if (actual.length !== cases.length) throw new Error("incomplete results");
+    const results = actual.map((result, i) => {
+      const pass = result.stdout === expected[i].stdout && result.status === expected[i].status;
+      return { name: result.name, pass, ...(!pass ? { expected: expected[i], actual: result } : {}) };
+    });
+    reports.push({ package: resolve(pkg), passed: results.filter(r => r.pass).length, total: cases.length, results });
+  }
+  console.log(JSON.stringify({ oracle: version, reports }, null, 2));
+  // Single-package mode is the CI gate; comparisons record old releases' failures.
+  if (packages.length === 1 && reports[0].passed !== cases.length) process.exitCode = 1;
+}
diff --git a/crates/bashkit-wasm/bench/results/wasm-startup-2026-10-15.json b/crates/bashkit-wasm/bench/results/wasm-startup-2026-10-15.json
new file mode 100644
index 000000000..f262816e4
--- /dev/null
+++ b/crates/bashkit-wasm/bench/results/wasm-startup-2026-10-15.json
@@ -0,0 +1,2064 @@
+{
+ "date": "2026-10-15",
+ "sourceBase": "8a817bf0",
+ "toolchain": {
+  "rust": "1.95.0",
+  "wasmBindgen": "0.2.128",
+  "binaryen": "125"
+ },
+ "published": {
+  "version": "0.18.2",
+  "gitHead": "567511386572d4c4b9f649ab1da97aad53b9f341",
+  "integrity": "sha512-WW8cdRvODapGKCtqh3h8foufAgUXjaL/fsd8Wt/qERqgubG024I/HLl5rQJnTQ8Nfj+UJdYBdj0EaTsv5s/vig=="
+ },
+ "compat": {
+  "oracle": "GNU bash, version 5.3.20(1)-release (aarch64-apple-darwin27.0.0)",
+  "reports": [
+   {
+    "package": "/private/tmp/bashkit-published/package",
+    "passed": 19,
+    "total": 24,
+    "results": [
+     {
+      "name": "quoted array",
+      "pass": true
+     },
+     {
+      "name": "sparse array",
+      "pass": false,
+      "expected": {
+       "stdout": "Mgo3CjIK",
+       "status": 0
+      },
+      "actual": {
+       "name": "sparse array",
+       "stdout": "MAoxCjIK",
+       "status": 0
+      }
+     },
+     {
+      "name": "array slice",
+      "pass": false,
+      "expected": {
+       "stdout": "ZAplCg==",
+       "status": 0
+      },
+      "actual": {
+       "name": "array slice",
+       "stdout": "ZCBlCg==",
+       "status": 0
+      }
+     },
+     {
+      "name": "associative array",
+      "pass": false,
+      "expected": {
+       "stdout": "dHdvIHdvcmRzCg==",
+       "status": 0
+      },
+      "actual": {
+       "name": "associative array",
+       "stdout": "J3R3bwo=",
+       "status": 0
+      }
+     },
+     {
+      "name": "nameref assignment",
+      "pass": true
+     },
+     {
+      "name": "nameref array",
+      "pass": true
+     },
+     {
+      "name": "case conversion",
+      "pass": true
+     },
+     {
+      "name": "pattern replacement",
+      "pass": true
+     },
+     {
+      "name": "indirect expansion",
+      "pass": true
+     },
+     {
+      "name": "dynamic scope",
+      "pass": true
+     },
+     {
+      "name": "arithmetic array",
+      "pass": false,
+      "expected": {
+       "stdout": "OAo=",
+       "status": 0
+      },
+      "actual": {
+       "name": "arithmetic array",
+       "stdout": "NQo=",
+       "status": 0
+      }
+     },
+     {
+      "name": "brace sequence",
+      "pass": false,
+      "expected": {
+       "stdout": "MDMKMDYKMDkK",
+       "status": 0
+      },
+      "actual": {
+       "name": "brace sequence",
+       "stdout": "Mwo2CjkK",
+       "status": 0
+      }
+     },
+     {
+      "name": "command substitution",
+      "pass": true
+     },
+     {
+      "name": "subshell isolation",
+      "pass": true
+     },
+     {
+      "name": "process substitution",
+      "pass": true
+     },
+     {
+      "name": "here string",
+      "pass": true
+     },
+     {
+      "name": "pipefail",
+      "pass": true
+     },
+     {
+      "name": "errexit exception",
+      "pass": true
+     },
+     {
+      "name": "case fallthrough",
+      "pass": true
+     },
+     {
+      "name": "regex capture",
+      "pass": true
+     },
+     {
+      "name": "mapfile",
+      "pass": true
+     },
+     {
+      "name": "child shell",
+      "pass": true
+     },
+     {
+      "name": "read delimiter",
+      "pass": true
+     },
+     {
+      "name": "negative status",
+      "pass": true
+     }
+    ]
+   },
+   {
+    "package": "/private/tmp/bashkit-tag-package",
+    "passed": 19,
+    "total": 24,
+    "results": [
+     {
+      "name": "quoted array",
+      "pass": true
+     },
+     {
+      "name": "sparse array",
+      "pass": false,
+      "expected": {
+       "stdout": "Mgo3CjIK",
+       "status": 0
+      },
+      "actual": {
+       "name": "sparse array",
+       "stdout": "MAoxCjIK",
+       "status": 0
+      }
+     },
+     {
+      "name": "array slice",
+      "pass": false,
+      "expected": {
+       "stdout": "ZAplCg==",
+       "status": 0
+      },
+      "actual": {
+       "name": "array slice",
+       "stdout": "ZCBlCg==",
+       "status": 0
+      }
+     },
+     {
+      "name": "associative array",
+      "pass": false,
+      "expected": {
+       "stdout": "dHdvIHdvcmRzCg==",
+       "status": 0
+      },
+      "actual": {
+       "name": "associative array",
+       "stdout": "J3R3bwo=",
+       "status": 0
+      }
+     },
+     {
+      "name": "nameref assignment",
+      "pass": true
+     },
+     {
+      "name": "nameref array",
+      "pass": true
+     },
+     {
+      "name": "case conversion",
+      "pass": true
+     },
+     {
+      "name": "pattern replacement",
+      "pass": true
+     },
+     {
+      "name": "indirect expansion",
+      "pass": true
+     },
+     {
+      "name": "dynamic scope",
+      "pass": true
+     },
+     {
+      "name": "arithmetic array",
+      "pass": false,
+      "expected": {
+       "stdout": "OAo=",
+       "status": 0
+      },
+      "actual": {
+       "name": "arithmetic array",
+       "stdout": "NQo=",
+       "status": 0
+      }
+     },
+     {
+      "name": "brace sequence",
+      "pass": false,
+      "expected": {
+       "stdout": "MDMKMDYKMDkK",
+       "status": 0
+      },
+      "actual": {
+       "name": "brace sequence",
+       "stdout": "Mwo2CjkK",
+       "status": 0
+      }
+     },
+     {
+      "name": "command substitution",
+      "pass": true
+     },
+     {
+      "name": "subshell isolation",
+      "pass": true
+     },
+     {
+      "name": "process substitution",
+      "pass": true
+     },
+     {
+      "name": "here string",
+      "pass": true
+     },
+     {
+      "name": "pipefail",
+      "pass": true
+     },
+     {
+      "name": "errexit exception",
+      "pass": true
+     },
+     {
+      "name": "case fallthrough",
+      "pass": true
+     },
+     {
+      "name": "regex capture",
+      "pass": true
+     },
+     {
+      "name": "mapfile",
+      "pass": true
+     },
+     {
+      "name": "child shell",
+      "pass": true
+     },
+     {
+      "name": "read delimiter",
+      "pass": true
+     },
+     {
+      "name": "negative status",
+      "pass": true
+     }
+    ]
+   },
+   {
+    "package": "/private/tmp/bashkit-wasm-startup/crates/bashkit-wasm/pkg",
+    "passed": 24,
+    "total": 24,
+    "results": [
+     {
+      "name": "quoted array",
+      "pass": true
+     },
+     {
+      "name": "sparse array",
+      "pass": true
+     },
+     {
+      "name": "array slice",
+      "pass": true
+     },
+     {
+      "name": "associative array",
+      "pass": true
+     },
+     {
+      "name": "nameref assignment",
+      "pass": true
+     },
+     {
+      "name": "nameref array",
+      "pass": true
+     },
+     {
+      "name": "case conversion",
+      "pass": true
+     },
+     {
+      "name": "pattern replacement",
+      "pass": true
+     },
+     {
+      "name": "indirect expansion",
+      "pass": true
+     },
+     {
+      "name": "dynamic scope",
+      "pass": true
+     },
+     {
+      "name": "arithmetic array",
+      "pass": true
+     },
+     {
+      "name": "brace sequence",
+      "pass": true
+     },
+     {
+      "name": "command substitution",
+      "pass": true
+     },
+     {
+      "name": "subshell isolation",
+      "pass": true
+     },
+     {
+      "name": "process substitution",
+      "pass": true
+     },
+     {
+      "name": "here string",
+      "pass": true
+     },
+     {
+      "name": "pipefail",
+      "pass": true
+     },
+     {
+      "name": "errexit exception",
+      "pass": true
+     },
+     {
+      "name": "case fallthrough",
+      "pass": true
+     },
+     {
+      "name": "regex capture",
+      "pass": true
+     },
+     {
+      "name": "mapfile",
+      "pass": true
+     },
+     {
+      "name": "child shell",
+      "pass": true
+     },
+     {
+      "name": "read delimiter",
+      "pass": true
+     },
+     {
+      "name": "negative status",
+      "pass": true
+     }
+    ]
+   }
+  ]
+ },
+ "profiles": {
+  "baseline": {
+   "files": [
+    {
+     "name": "index.js",
+     "sha256": "1583d806464a708b29a27fa4f9cd2c3299df6e142fe301043a94c30b4b813cae",
+     "raw": 1503,
+     "gzip": 828,
+     "brotli": 673
+    },
+    {
+     "name": "bashkit_wasm.js",
+     "sha256": "3b03202632bfae9333b32b2eebbb69519962ef3e5d0f6ae35546ee67fa1b9fa9",
+     "raw": 65159,
+     "gzip": 8704,
+     "brotli": 7481
+    },
+    {
+     "name": "bashkit_wasm_bg.wasm",
+     "sha256": "f59d5c508f7405c03400a1c46ab1bc7e06fc352c540abfbff6ba197548d9b1b2",
+     "raw": 9452877,
+     "gzip": 3560745,
+     "brotli": 2460299
+    }
+   ],
+   "total": {
+    "raw": 9519539,
+    "gzip": 3570277,
+    "brotli": 2468453
+   },
+   "sections": [
+    {
+     "name": "section-1",
+     "raw": 1008
+    },
+    {
+     "name": "section-2",
+     "raw": 3841
+    },
+    {
+     "name": "section-3",
+     "raw": 8439
+    },
+    {
+     "name": "section-4",
+     "raw": 7
+    },
+    {
+     "name": "section-5",
+     "raw": 3
+    },
+    {
+     "name": "section-6",
+     "raw": 25
+    },
+    {
+     "name": "section-7",
+     "raw": 1819
+    },
+    {
+     "name": "section-9",
+     "raw": 8164
+    },
+    {
+     "name": "section-12",
+     "raw": 2
+    },
+    {
+     "name": "code",
+     "raw": 8279718
+    },
+    {
+     "name": "data",
+     "raw": 1149749
+    },
+    {
+     "name": "custom:producers",
+     "raw": 60
+    }
+   ]
+  },
+  "selected": {
+   "files": [
+    {
+     "name": "index.js",
+     "sha256": "1583d806464a708b29a27fa4f9cd2c3299df6e142fe301043a94c30b4b813cae",
+     "raw": 1503,
+     "gzip": 828,
+     "brotli": 673
+    },
+    {
+     "name": "bashkit_wasm.js",
+     "sha256": "15ab168775e5a4623bfb4d0164f34dcdc78f8b73aaa1e0a0063cfc1aafed207d",
+     "raw": 65168,
+     "gzip": 8705,
+     "brotli": 7499
+    },
+    {
+     "name": "bashkit_wasm_bg.wasm",
+     "sha256": "d65414678c9122205486686cd3c1f0296a92129c046229b2bcef9e2d59ff2ea7",
+     "raw": 7602233,
+     "gzip": 2963968,
+     "brotli": 2117510
+    }
+   ],
+   "total": {
+    "raw": 7668904,
+    "gzip": 2973501,
+    "brotli": 2125682
+   },
+   "sections": [
+    {
+     "name": "section-1",
+     "raw": 913
+    },
+    {
+     "name": "section-2",
+     "raw": 3841
+    },
+    {
+     "name": "section-3",
+     "raw": 10594
+    },
+    {
+     "name": "section-4",
+     "raw": 7
+    },
+    {
+     "name": "section-5",
+     "raw": 3
+    },
+    {
+     "name": "section-6",
+     "raw": 25
+    },
+    {
+     "name": "section-7",
+     "raw": 1822
+    },
+    {
+     "name": "section-9",
+     "raw": 8076
+    },
+    {
+     "name": "section-12",
+     "raw": 2
+    },
+    {
+     "name": "code",
+     "raw": 6433520
+    },
+    {
+     "name": "data",
+     "raw": 1143328
+    },
+    {
+     "name": "custom:producers",
+     "raw": 60
+    }
+   ]
+  },
+  "rejectedZ": {
+   "files": [
+    {
+     "name": "index.js",
+     "sha256": "1583d806464a708b29a27fa4f9cd2c3299df6e142fe301043a94c30b4b813cae",
+     "raw": 1503,
+     "gzip": 828,
+     "brotli": 673
+    },
+    {
+     "name": "bashkit_wasm.js",
+     "sha256": "91c7a36d8fe29a8bb99b4f0dad6113664aedb95e6f58cf30f40ef6c298f1beb1",
+     "raw": 65168,
+     "gzip": 8707,
+     "brotli": 7479
+    },
+    {
+     "name": "bashkit_wasm_bg.wasm",
+     "sha256": "e532fe6b404c546e50e553947672ed56715cf5b5b854872517a1cb29a693881c",
+     "raw": 6353397,
+     "gzip": 2496856,
+     "brotli": 1843216
+    }
+   ],
+   "total": {
+    "raw": 6420068,
+    "gzip": 2506391,
+    "brotli": 1851368
+   },
+   "sections": [
+    {
+     "name": "section-1",
+     "raw": 1031
+    },
+    {
+     "name": "section-2",
+     "raw": 3841
+    },
+    {
+     "name": "section-3",
+     "raw": 13682
+    },
+    {
+     "name": "section-4",
+     "raw": 7
+    },
+    {
+     "name": "section-5",
+     "raw": 3
+    },
+    {
+     "name": "section-6",
+     "raw": 25
+    },
+    {
+     "name": "section-7",
+     "raw": 1822
+    },
+    {
+     "name": "section-9",
+     "raw": 8335
+    },
+    {
+     "name": "section-12",
+     "raw": 2
+    },
+    {
+     "name": "code",
+     "raw": 5149690
+    },
+    {
+     "name": "data",
+     "raw": 1174857
+    },
+    {
+     "name": "custom:producers",
+     "raw": 60
+    }
+   ]
+  }
+ },
+ "cold": {
+  "method": "Cold context, Brotli 11, CDP Fast/Slow 4G, page CPU 4x; main-thread JS-host WASM, no terminal renderer dependency; prompt after first echo and two animation frames",
+  "browser": "155.0.8059.79",
+  "node": "v26.5.0",
+  "packages": [
+   {
+    "path": "/private/tmp/bashkit-baseline-oz",
+    "files": [
+     {
+      "name": "index.js",
+      "sha256": "1583d806464a708b29a27fa4f9cd2c3299df6e142fe301043a94c30b4b813cae",
+      "raw": 1503,
+      "gzip": 828,
+      "brotli": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "sha256": "3b03202632bfae9333b32b2eebbb69519962ef3e5d0f6ae35546ee67fa1b9fa9",
+      "raw": 65159,
+      "gzip": 8704,
+      "brotli": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "sha256": "f59d5c508f7405c03400a1c46ab1bc7e06fc352c540abfbff6ba197548d9b1b2",
+      "raw": 9452877,
+      "gzip": 3560745,
+      "brotli": 2460299
+     }
+    ],
+    "total": {
+     "raw": 9519539,
+     "gzip": 3570277,
+     "brotli": 2468453
+    },
+    "sections": [
+     {
+      "name": "section-1",
+      "raw": 1008
+     },
+     {
+      "name": "section-2",
+      "raw": 3841
+     },
+     {
+      "name": "section-3",
+      "raw": 8439
+     },
+     {
+      "name": "section-4",
+      "raw": 7
+     },
+     {
+      "name": "section-5",
+      "raw": 3
+     },
+     {
+      "name": "section-6",
+      "raw": 25
+     },
+     {
+      "name": "section-7",
+      "raw": 1819
+     },
+     {
+      "name": "section-9",
+      "raw": 8164
+     },
+     {
+      "name": "section-12",
+      "raw": 2
+     },
+     {
+      "name": "code",
+      "raw": 8279718
+     },
+     {
+      "name": "data",
+      "raw": 1149749
+     },
+     {
+      "name": "custom:producers",
+      "raw": 60
+     }
+    ]
+   },
+   {
+    "path": "/private/tmp/bashkit-balanced-profile-oz",
+    "files": [
+     {
+      "name": "index.js",
+      "sha256": "1583d806464a708b29a27fa4f9cd2c3299df6e142fe301043a94c30b4b813cae",
+      "raw": 1503,
+      "gzip": 828,
+      "brotli": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "sha256": "15ab168775e5a4623bfb4d0164f34dcdc78f8b73aaa1e0a0063cfc1aafed207d",
+      "raw": 65168,
+      "gzip": 8705,
+      "brotli": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "sha256": "d65414678c9122205486686cd3c1f0296a92129c046229b2bcef9e2d59ff2ea7",
+      "raw": 7602233,
+      "gzip": 2963968,
+      "brotli": 2117510
+     }
+    ],
+    "total": {
+     "raw": 7668904,
+     "gzip": 2973501,
+     "brotli": 2125682
+    },
+    "sections": [
+     {
+      "name": "section-1",
+      "raw": 913
+     },
+     {
+      "name": "section-2",
+      "raw": 3841
+     },
+     {
+      "name": "section-3",
+      "raw": 10594
+     },
+     {
+      "name": "section-4",
+      "raw": 7
+     },
+     {
+      "name": "section-5",
+      "raw": 3
+     },
+     {
+      "name": "section-6",
+      "raw": 25
+     },
+     {
+      "name": "section-7",
+      "raw": 1822
+     },
+     {
+      "name": "section-9",
+      "raw": 8076
+     },
+     {
+      "name": "section-12",
+      "raw": 2
+     },
+     {
+      "name": "code",
+      "raw": 6433520
+     },
+     {
+      "name": "data",
+      "raw": 1143328
+     },
+     {
+      "name": "custom:producers",
+      "raw": 60
+     }
+    ]
+   }
+  ],
+  "samples": [
+   {
+    "package": 0,
+    "condition": "local",
+    "trial": 0,
+    "marks": {
+     "start": 21.100000023841858,
+     "imported": 55.39999997615814,
+     "initialized": 135,
+     "command": 191.69999992847443,
+     "prompt": 222.79999995231628
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 22.100000023841858,
+      "end": 25.100000023841858,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 40.60000002384186,
+      "end": 43.5,
+      "transfer": 7781,
+      "encoded": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 57.10000002384186,
+      "end": 96.89999997615814,
+      "transfer": 2460599,
+      "encoded": 2460299
+     },
+     {
+      "name": "favicon.ico",
+      "start": 73.19999992847443,
+      "end": 74,
+      "transfer": 300,
+      "encoded": 0
+     }
+    ]
+   },
+   {
+    "package": 1,
+    "condition": "local",
+    "trial": 0,
+    "marks": {
+     "start": 12.899999976158142,
+     "imported": 37.39999997615814,
+     "initialized": 100,
+     "command": 147.29999995231628,
+     "prompt": 172.20000004768372
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 13.899999976158142,
+      "end": 17.299999952316284,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 18.399999976158142,
+      "end": 19.100000023841858,
+      "transfer": 7799,
+      "encoded": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 39.299999952316284,
+      "end": 63.700000047683716,
+      "transfer": 2117810,
+      "encoded": 2117510
+     }
+    ]
+   },
+   {
+    "package": 0,
+    "condition": "local",
+    "trial": 1,
+    "marks": {
+     "start": 17.399999976158142,
+     "imported": 41.89999997615814,
+     "initialized": 116.29999995231628,
+     "command": 173.5,
+     "prompt": 200.5
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 18.899999976158142,
+      "end": 21.199999928474426,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 22.399999976158142,
+      "end": 22.899999976158142,
+      "transfer": 7781,
+      "encoded": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 43.299999952316284,
+      "end": 74.5,
+      "transfer": 2460599,
+      "encoded": 2460299
+     }
+    ]
+   },
+   {
+    "package": 1,
+    "condition": "local",
+    "trial": 1,
+    "marks": {
+     "start": 18.199999928474426,
+     "imported": 40.199999928474426,
+     "initialized": 100.19999992847443,
+     "command": 148.10000002384186,
+     "prompt": 173.89999997615814
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 19.199999928474426,
+      "end": 21.899999976158142,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 22.899999976158142,
+      "end": 24,
+      "transfer": 7799,
+      "encoded": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 41.5,
+      "end": 65.10000002384186,
+      "transfer": 2117810,
+      "encoded": 2117510
+     }
+    ]
+   },
+   {
+    "package": 0,
+    "condition": "local",
+    "trial": 2,
+    "marks": {
+     "start": 20.899999976158142,
+     "imported": 45.39999997615814,
+     "initialized": 113.39999997615814,
+     "command": 168.69999992847443,
+     "prompt": 196.59999990463257
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 21.899999976158142,
+      "end": 24.5,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 25.299999952316284,
+      "end": 25.799999952316284,
+      "transfer": 7781,
+      "encoded": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 47.39999997615814,
+      "end": 74.39999997615814,
+      "transfer": 2460599,
+      "encoded": 2460299
+     }
+    ]
+   },
+   {
+    "package": 1,
+    "condition": "local",
+    "trial": 2,
+    "marks": {
+     "start": 17.300000071525574,
+     "imported": 24,
+     "initialized": 95.40000009536743,
+     "command": 141.90000009536743,
+     "prompt": 168
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 18.200000047683716,
+      "end": 20.800000071525574,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 21.800000071525574,
+      "end": 22.600000023841858,
+      "transfer": 7799,
+      "encoded": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 25.90000009536743,
+      "end": 61.40000009536743,
+      "transfer": 2117810,
+      "encoded": 2117510
+     }
+    ]
+   },
+   {
+    "package": 0,
+    "condition": "local",
+    "trial": 3,
+    "marks": {
+     "start": 16.100000023841858,
+     "imported": 39.699999928474426,
+     "initialized": 106.29999995231628,
+     "command": 162.19999992847443,
+     "prompt": 189.89999997615814
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 16.699999928474426,
+      "end": 19.699999928474426,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 20.899999976158142,
+      "end": 22.199999928474426,
+      "transfer": 7781,
+      "encoded": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 41.799999952316284,
+      "end": 68.5,
+      "transfer": 2460599,
+      "encoded": 2460299
+     }
+    ]
+   },
+   {
+    "package": 1,
+    "condition": "local",
+    "trial": 3,
+    "marks": {
+     "start": 17.09999990463257,
+     "imported": 39.59999990463257,
+     "initialized": 94,
+     "command": 140,
+     "prompt": 163.89999997615814
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 17.899999976158142,
+      "end": 20.299999952316284,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 21.5,
+      "end": 21.899999976158142,
+      "transfer": 7799,
+      "encoded": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 41,
+      "end": 62.59999990463257,
+      "transfer": 2117810,
+      "encoded": 2117510
+     }
+    ]
+   },
+   {
+    "package": 0,
+    "condition": "local",
+    "trial": 4,
+    "marks": {
+     "start": 16.700000047683716,
+     "imported": 40.89999997615814,
+     "initialized": 105.10000002384186,
+     "command": 160.39999997615814,
+     "prompt": 186.39999997615814
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 17.399999976158142,
+      "end": 20.800000071525574,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 22.300000071525574,
+      "end": 23,
+      "transfer": 7781,
+      "encoded": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 42,
+      "end": 66.5,
+      "transfer": 2460599,
+      "encoded": 2460299
+     }
+    ]
+   },
+   {
+    "package": 1,
+    "condition": "local",
+    "trial": 4,
+    "marks": {
+     "start": 14.899999976158142,
+     "imported": 37.199999928474426,
+     "initialized": 94.79999995231628,
+     "command": 140.19999992847443,
+     "prompt": 163.29999995231628
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 15.899999976158142,
+      "end": 18.699999928474426,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 19.899999976158142,
+      "end": 20.699999928474426,
+      "transfer": 7799,
+      "encoded": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 38.799999952316284,
+      "end": 59.199999928474426,
+      "transfer": 2117810,
+      "encoded": 2117510
+     }
+    ]
+   },
+   {
+    "package": 0,
+    "condition": "fast4g",
+    "trial": 0,
+    "marks": {
+     "start": 188.39999997615814,
+     "imported": 545.8999999761581,
+     "initialized": 3229.100000023842,
+     "command": 3284,
+     "prompt": 3309.5
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 189.10000002384186,
+      "end": 361.6999999284744,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 362.6999999284744,
+      "end": 544.3999999761581,
+      "transfer": 7781,
+      "encoded": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 547.3999999761581,
+      "end": 3189.399999976158,
+      "transfer": 2460599,
+      "encoded": 2460299
+     }
+    ]
+   },
+   {
+    "package": 1,
+    "condition": "fast4g",
+    "trial": 0,
+    "marks": {
+     "start": 190.5,
+     "imported": 544.8999999761581,
+     "initialized": 2842.3000000715256,
+     "command": 2888.5,
+     "prompt": 2913.8000000715256
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 190.80000007152557,
+      "end": 365.8000000715256,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 366.8000000715256,
+      "end": 543.8999999761581,
+      "transfer": 7799,
+      "encoded": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 546.8999999761581,
+      "end": 2809.899999976158,
+      "transfer": 2117810,
+      "encoded": 2117510
+     }
+    ]
+   },
+   {
+    "package": 0,
+    "condition": "fast4g",
+    "trial": 1,
+    "marks": {
+     "start": 189.79999995231628,
+     "imported": 552.1000000238419,
+     "initialized": 3196.5,
+     "command": 3251.899999976158,
+     "prompt": 3278.600000023842
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 190.69999992847443,
+      "end": 367.2999999523163,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 368.6999999284744,
+      "end": 550.5,
+      "transfer": 7781,
+      "encoded": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 553.1999999284744,
+      "end": 3154.399999976158,
+      "transfer": 2460599,
+      "encoded": 2460299
+     }
+    ]
+   },
+   {
+    "package": 1,
+    "condition": "fast4g",
+    "trial": 1,
+    "marks": {
+     "start": 187.90000009536743,
+     "imported": 544.5,
+     "initialized": 2847.3000000715256,
+     "command": 2893.3000000715256,
+     "prompt": 2917.8000000715256
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 188.80000007152557,
+      "end": 358.5,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 359.7000000476837,
+      "end": 542.5,
+      "transfer": 7799,
+      "encoded": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 545.6000000238419,
+      "end": 2809.8000000715256,
+      "transfer": 2117810,
+      "encoded": 2117510
+     }
+    ]
+   },
+   {
+    "package": 0,
+    "condition": "fast4g",
+    "trial": 2,
+    "marks": {
+     "start": 189.70000004768372,
+     "imported": 546,
+     "initialized": 3199.899999976158,
+     "command": 3255.100000023842,
+     "prompt": 3282.2000000476837
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 190.70000004768372,
+      "end": 365.5,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 365.89999997615814,
+      "end": 544.7000000476837,
+      "transfer": 7781,
+      "encoded": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 547.6000000238419,
+      "end": 3152.2000000476837,
+      "transfer": 2460599,
+      "encoded": 2460299
+     }
+    ]
+   },
+   {
+    "package": 1,
+    "condition": "fast4g",
+    "trial": 2,
+    "marks": {
+     "start": 188.89999997615814,
+     "imported": 543.8999999761581,
+     "initialized": 2847.7000000476837,
+     "command": 2893.7000000476837,
+     "prompt": 2919.5
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 189.89999997615814,
+      "end": 361.5,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 362.10000002384186,
+      "end": 542.5,
+      "transfer": 7799,
+      "encoded": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 546,
+      "end": 2811,
+      "transfer": 2117810,
+      "encoded": 2117510
+     }
+    ]
+   },
+   {
+    "package": 0,
+    "condition": "fast4g",
+    "trial": 3,
+    "marks": {
+     "start": 189.10000002384186,
+     "imported": 549.6000000238419,
+     "initialized": 3203,
+     "command": 3258.600000023842,
+     "prompt": 3285.100000023842
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 190.5,
+      "end": 364.39999997615814,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 365.8000000715256,
+      "end": 548,
+      "transfer": 7781,
+      "encoded": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 551.1000000238419,
+      "end": 3154.5,
+      "transfer": 2460599,
+      "encoded": 2460299
+     }
+    ]
+   },
+   {
+    "package": 1,
+    "condition": "fast4g",
+    "trial": 3,
+    "marks": {
+     "start": 189,
+     "imported": 544.6999999284744,
+     "initialized": 2852.6999999284744,
+     "command": 2898.600000023842,
+     "prompt": 2921.600000023842
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 189.89999997615814,
+      "end": 364.7999999523163,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 365.39999997615814,
+      "end": 542.6999999284744,
+      "transfer": 7799,
+      "encoded": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 546.1999999284744,
+      "end": 2813.100000023842,
+      "transfer": 2117810,
+      "encoded": 2117510
+     }
+    ]
+   },
+   {
+    "package": 0,
+    "condition": "fast4g",
+    "trial": 4,
+    "marks": {
+     "start": 190.30000007152557,
+     "imported": 546.5,
+     "initialized": 3236.100000023842,
+     "command": 3294.3000000715256,
+     "prompt": 3326.5
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 190.60000002384186,
+      "end": 366.60000002384186,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 367.10000002384186,
+      "end": 545.1000000238419,
+      "transfer": 7781,
+      "encoded": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 548.8000000715256,
+      "end": 3192.100000023842,
+      "transfer": 2460599,
+      "encoded": 2460299
+     }
+    ]
+   },
+   {
+    "package": 1,
+    "condition": "fast4g",
+    "trial": 4,
+    "marks": {
+     "start": 190.5,
+     "imported": 546.8999999761581,
+     "initialized": 2876.600000023842,
+     "command": 2921.7000000476837,
+     "prompt": 2945.600000023842
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 190.70000004768372,
+      "end": 363.2000000476837,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 365,
+      "end": 545.5,
+      "transfer": 7799,
+      "encoded": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 548.2000000476837,
+      "end": 2842.399999976158,
+      "transfer": 2117810,
+      "encoded": 2117510
+     }
+    ]
+   },
+   {
+    "package": 0,
+    "condition": "slow4g",
+    "trial": 0,
+    "marks": {
+     "start": 599.3999999761581,
+     "imported": 1792.8999999761581,
+     "initialized": 16080.699999928474,
+     "command": 16137.599999904633,
+     "prompt": 16166.399999976158
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 600.5,
+      "end": 1172.5,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 1174.1999999284744,
+      "end": 1791.6999999284744,
+      "transfer": 7781,
+      "encoded": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 1794.6999999284744,
+      "end": 16040.099999904633,
+      "transfer": 2460599,
+      "encoded": 2460299
+     }
+    ]
+   },
+   {
+    "package": 1,
+    "condition": "slow4g",
+    "trial": 0,
+    "marks": {
+     "start": 595.5,
+     "imported": 1799.2000000476837,
+     "initialized": 14169.300000071526,
+     "command": 14216.600000023842,
+     "prompt": 14244.200000047684
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 596.5,
+      "end": 1177.9000000953674,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 1179.5,
+      "end": 1797.8000000715256,
+      "transfer": 7799,
+      "encoded": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 1801.4000000953674,
+      "end": 14135.400000095367,
+      "transfer": 2117810,
+      "encoded": 2117510
+     }
+    ]
+   },
+   {
+    "package": 0,
+    "condition": "slow4g",
+    "trial": 1,
+    "marks": {
+     "start": 589.6999999284744,
+     "imported": 1792.5,
+     "initialized": 16091.099999904633,
+     "command": 16148.099999904633,
+     "prompt": 16177.199999928474
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 591.3999999761581,
+      "end": 1174.3999999761581,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 1175.5999999046326,
+      "end": 1791,
+      "transfer": 7781,
+      "encoded": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 1794,
+      "end": 16048.399999976158,
+      "transfer": 2460599,
+      "encoded": 2460299
+     }
+    ]
+   },
+   {
+    "package": 1,
+    "condition": "slow4g",
+    "trial": 1,
+    "marks": {
+     "start": 590.6000000238419,
+     "imported": 1801.7000000476837,
+     "initialized": 14179,
+     "command": 14225.5,
+     "prompt": 14250.800000071526
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 591.6000000238419,
+      "end": 1174,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 1175.2000000476837,
+      "end": 1799.6000000238419,
+      "transfer": 7799,
+      "encoded": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 1803.5,
+      "end": 14141.5,
+      "transfer": 2117810,
+      "encoded": 2117510
+     }
+    ]
+   },
+   {
+    "package": 0,
+    "condition": "slow4g",
+    "trial": 2,
+    "marks": {
+     "start": 589.5,
+     "imported": 1784.6000000238419,
+     "initialized": 16065,
+     "command": 16121.300000071526,
+     "prompt": 16148.600000023842
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 590.3999999761581,
+      "end": 1164.7000000476837,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 1165.5,
+      "end": 1783,
+      "transfer": 7781,
+      "encoded": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 1786.8000000715256,
+      "end": 16022.300000071526,
+      "transfer": 2460599,
+      "encoded": 2460299
+     }
+    ]
+   },
+   {
+    "package": 1,
+    "condition": "slow4g",
+    "trial": 2,
+    "marks": {
+     "start": 587.8000000715256,
+     "imported": 1784.8000000715256,
+     "initialized": 14156.600000023842,
+     "command": 14202.700000047684,
+     "prompt": 14227.300000071526
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 588.7000000476837,
+      "end": 1165.6000000238419,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 1167,
+      "end": 1783.2000000476837,
+      "transfer": 7799,
+      "encoded": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 1786.8000000715256,
+      "end": 14117.300000071526,
+      "transfer": 2117810,
+      "encoded": 2117510
+     }
+    ]
+   },
+   {
+    "package": 0,
+    "condition": "slow4g",
+    "trial": 3,
+    "marks": {
+     "start": 597.7000000476837,
+     "imported": 1799.6000000238419,
+     "initialized": 16093.5,
+     "command": 16150.299999952316,
+     "prompt": 16177.600000023842
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 598.8999999761581,
+      "end": 1181.2000000476837,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 1182.2999999523163,
+      "end": 1797.7999999523163,
+      "transfer": 7781,
+      "encoded": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 1800.7000000476837,
+      "end": 16047.899999976158,
+      "transfer": 2460599,
+      "encoded": 2460299
+     }
+    ]
+   },
+   {
+    "package": 1,
+    "condition": "slow4g",
+    "trial": 3,
+    "marks": {
+     "start": 601.3999999761581,
+     "imported": 1792.6000000238419,
+     "initialized": 14169,
+     "command": 14221.699999928474,
+     "prompt": 14253.5
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 602,
+      "end": 1172.5,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 1173.7999999523163,
+      "end": 1791.1999999284744,
+      "transfer": 7799,
+      "encoded": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 1794.8999999761581,
+      "end": 14130.100000023842,
+      "transfer": 2117810,
+      "encoded": 2117510
+     }
+    ]
+   },
+   {
+    "package": 0,
+    "condition": "slow4g",
+    "trial": 4,
+    "marks": {
+     "start": 595.1000000238419,
+     "imported": 1799.1000000238419,
+     "initialized": 16091.600000023842,
+     "command": 16149.400000095367,
+     "prompt": 16177.100000023842
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 596.4000000953674,
+      "end": 1180.5,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 1183.8000000715256,
+      "end": 1797.2000000476837,
+      "transfer": 7781,
+      "encoded": 7481
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 1801.5,
+      "end": 16043.400000095367,
+      "transfer": 2460599,
+      "encoded": 2460299
+     }
+    ]
+   },
+   {
+    "package": 1,
+    "condition": "slow4g",
+    "trial": 4,
+    "marks": {
+     "start": 597.5,
+     "imported": 1811.3000000715256,
+     "initialized": 14179.800000071526,
+     "command": 14225.900000095367,
+     "prompt": 14250.700000047684
+    },
+    "resources": [
+     {
+      "name": "index.js",
+      "start": 598.6000000238419,
+      "end": 1184.3000000715256,
+      "transfer": 973,
+      "encoded": 673
+     },
+     {
+      "name": "bashkit_wasm.js",
+      "start": 1185.5,
+      "end": 1809.1000000238419,
+      "transfer": 7799,
+      "encoded": 7499
+     },
+     {
+      "name": "bashkit_wasm_bg.wasm",
+      "start": 1812.8000000715256,
+      "end": 14139.600000023842,
+      "transfer": 2117810,
+      "encoded": 2117510
+     }
+    ]
+   }
+  ]
+ },
+ "runtime": {
+  "baseline": {
+   "node": "v26.5.0",
+   "package": "/private/tmp/bashkit-baseline-oz",
+   "results": {
+    "arithmetic": {
+     "stdout": "199990000\n",
+     "samples": [
+      44.23554200000001,
+      44.316374999999994,
+      43.21366599999999,
+      44.59429100000003,
+      44.638458000000014,
+      43.765625,
+      43.305542
+     ],
+     "median": 44.23554200000001
+    },
+    "substitutions": {
+     "stdout": "hi\n",
+     "samples": [
+      6.691499999999962,
+      6.413416999999981,
+      9.098292000000015,
+      6.096458999999982,
+      5.677875000000029,
+      6.283957999999984,
+      5.434708999999998
+     ],
+     "median": 6.283957999999984
+    },
+    "jq": {
+     "stdout": "1001000\n",
+     "samples": [
+      2.5480830000000196,
+      2.786707999999976,
+      2.460124999999948,
+      2.3708750000000123,
+      2.6324579999999855,
+      2.233041000000071,
+      2.3139159999999492
+     ],
+     "median": 2.460124999999948
+    },
+    "text": {
+     "stdout": "10000\n",
+     "samples": [
+      10.493916000000013,
+      10.508416000000011,
+      9.975500000000011,
+      9.83141599999999,
+      9.62195799999995,
+      9.435292000000004,
+      9.550375000000031
+     ],
+     "median": 9.83141599999999
+    }
+   }
+  },
+  "selected": {
+   "node": "v26.5.0",
+   "package": "/private/tmp/bashkit-balanced-profile-oz",
+   "results": {
+    "arithmetic": {
+     "stdout": "199990000\n",
+     "samples": [
+      50.007542000000015,
+      48.53175000000002,
+      48.67329200000003,
+      50.149541,
+      49.739832999999976,
+      49.493041000000005,
+      48.65091699999999
+     ],
+     "median": 49.493041000000005
+    },
+    "substitutions": {
+     "stdout": "hi\n",
+     "samples": [
+      7.53895799999998,
+      6.6450419999999895,
+      6.147041999999999,
+      6.18829199999999,
+      6.094959000000017,
+      5.852458000000013,
+      5.859375
+     ],
+     "median": 6.147041999999999
+    },
+    "jq": {
+     "stdout": "1001000\n",
+     "samples": [
+      2.4794160000000147,
+      2.3771659999999883,
+      2.6838749999999436,
+      2.2523749999999154,
+      2.1724590000000035,
+      2.162667000000056,
+      2.2771249999999554
+     ],
+     "median": 2.2771249999999554
+    },
+    "text": {
+     "stdout": "10000\n",
+     "samples": [
+      11.623333000000002,
+      11.464875000000006,
+      10.260125000000016,
+      10.209958000000029,
+      10.119290999999976,
+      9.871250000000032,
+      10.29849999999999
+     ],
+     "median": 10.260125000000016
+    }
+   }
+  },
+  "rejectedZ": {
+   "node": "v26.5.0",
+   "package": "/private/tmp/bashkit-size-profile-oz",
+   "results": {
+    "arithmetic": {
+     "stdout": "199990000\n",
+     "samples": [
+      66.689583,
+      66.24974999999998,
+      66.00487499999997,
+      66.73350000000005,
+      66.180791,
+      66.12816700000002,
+      65.74704099999997
+     ],
+     "median": 66.180791
+    },
+    "substitutions": {
+     "stdout": "hi\n",
+     "samples": [
+      8.666624999999954,
+      7.969583000000057,
+      7.808374999999955,
+      7.881167000000005,
+      7.785916000000043,
+      7.719707999999969,
+      7.606457999999975
+     ],
+     "median": 7.808374999999955
+    },
+    "jq": {
+     "stdout": "1001000\n",
+     "samples": [
+      3.4603329999999914,
+      3.604332999999997,
+      3.381917000000044,
+      3.0702499999999873,
+      2.683917000000065,
+      2.3656250000000227,
+      2.283625000000029
+     ],
+     "median": 3.0702499999999873
+    },
+    "text": {
+     "stdout": "10000\n",
+     "samples": [
+      13.437832999999955,
+      13.11283299999991,
+      12.069624999999974,
+      11.965250000000083,
+      11.758916999999997,
+      11.905624999999986,
+      11.672791999999959
+     ],
+     "median": 11.965250000000083
+    }
+   }
+  }
+ }
+}
diff --git a/crates/bashkit-wasm/bench/results/wasm-startup-2026-10-15.md b/crates/bashkit-wasm/bench/results/wasm-startup-2026-10-15.md
new file mode 100644
index 000000000..542b7ddc5
--- /dev/null
+++ b/crates/bashkit-wasm/bench/results/wasm-startup-2026-10-15.md
@@ -0,0 +1,133 @@
+# Browser package compatibility and cold start — 2026-10-15
+
+Bashkit's published/source compatibility gap is version drift. The npm 0.18.2
+artifact identifies release commit `567511386572d4c4b9f649ab1da97aad53b9f341`
+(September 22), while the independent study's source is `491c3281`, 269 commits
+newer. The source and published distributions use the same build path. The
+package version remains 0.18.2 on unreleased main; it is not an artifact identity.
+
+A dedicated `wasm-release` profile (`opt-level = "s"`, fat LTO, Binaryen 125
+`-Oz`) reduces current-package Brotli transfer by **13.9%** without dropping
+`jq`, `Terminal`, commands, or resource limits. Native release settings stay
+unchanged. A more aggressive `"z"` profile saved 25.0% but slowed the arithmetic
+sample about 50%, so it was rejected.
+
+## Independent evidence and validation scope
+
+- [brush-web compatibility study](https://github.com/ShaulLavo/brush-web/pull/7):
+  Oils / Brush / Bashkit corpora reported source 95.4 / 84.1 / 96.2% versus
+  published 55.1 / 55.8 / 70.8% for builtins-only cases. Its own report attributes
+  the old package's failures to later compatibility work.
+- [mobile cold-start study](https://github.com/ShaulLavo/brush-web/pull/9):
+  transfer dominated startup; source grew across those 269 commits; removing
+  Terminal saved only about 50 KB Brotli; `wasm-opt -Oz` alone did not help.
+- [initial API comparison](https://github.com/ShaulLavo/brush-web/pull/5):
+  the published package lacked the source tree's newer Terminal API.
+
+This investigation independently checks npm `gitHead` against the release tag,
+compares 24 bounded GNU Bash cases, rebuilds the exact release source, and measures
+real Chromium startup of the full current package. The 24 cases are a regression
+sample, **not a rerun or independent confirmation of the three large-corpus
+percentages above**. They isolate representative release gaps without turning a
+large, evolving third-party corpus into a fragile release gate.
+
+## Compatibility
+
+GNU Bash 5.3.20 is the oracle. Fresh shell per case; exact stdout bytes and exit
+status; stderr not scored. Results are in the matching JSON report.
+
+| Artifact | Cases passed |
+|---|---:|
+| npm 0.18.2 | 19/24 |
+| Rebuilt v0.18.2 source tag | 19/24, identical five failures |
+| Current source, original release profile | 24/24 |
+| Current source, selected shipping profile | 24/24 |
+
+Published failures: sparse array indices, negative array slices, associative
+array literals, arithmetic array updates, zero-padded brace sequences. The
+existing source fixes remain intact under the new profile. All 70 browser
+package integration/packaging tests pass, including async host filesystem,
+timers, cancellation/resource limits, jq, binary output, and interactive terminal.
+
+## Artifact attribution
+
+Same source base `8a817bf0`, Rust 1.95.0, wasm-bindgen 0.2.128, single-threaded
+`wasm32-unknown-unknown`, `terminal` enabled, and pinned Binaryen 125 `-Oz` for
+both baseline and selected builds. Byte counts include `index.js`, generated JS
+glue, and the WASM; compression is per file, as served by a CDN.
+
+| Profile | Raw bytes | gzip-9 bytes | Brotli-11 bytes |
+|---|---:|---:|---:|
+| Original release: opt 3, thin LTO | 9,519,539 | 3,570,277 | 2,468,453 |
+| Selected: opt s, fat LTO | 7,668,904 | 2,973,501 | 2,125,682 |
+| Rejected: opt z, fat LTO | 6,420,068 | 2,506,391 | 1,851,368 |
+
+The selected build saves 342,771 Brotli bytes (13.9%), 596,776 gzip bytes (16.7%),
+and 1,850,635 raw bytes (19.4%). Its WASM is 99.6% of the Brotli load graph;
+wrapper + glue total about 8 KB. The WASM code section falls from 8,279,718 to
+6,433,520 bytes; data stays roughly constant (1,149,749 to 1,143,328 bytes).
+The improvement removes generated code overhead rather than command capability
+or symbol names: the baseline has no large debug/name custom section to strip.
+
+The old npm artifact is 1,826,367 Brotli bytes for the same three-file graph.
+Current source carries substantially more behavior. Even after this change,
+that old artifact remains smaller; publishing the repaired source does not mean
+restoring the old feature set or promising just-bash's much smaller payload.
+
+## Cold startup
+
+Five trials per artifact/condition, fresh browser context (cold HTTP cache),
+rotated order, Chrome 155.0.8059.79, Node 26.5.0, macOS Apple Silicon. Brotli-11
+loopback server and CDP network throttling, page CPU 4x. Both artifacts use the
+real loader and must run `echo hi`, drive Terminal, and paint the result. Samples
+and resource timings are retained. Full method: [experiment guide](../README.md).
+
+| Condition | Baseline median | Selected median | Saved |
+|---|---:|---:|---:|
+| Loopback + page CPU 4x | 196.6 ms | 168.0 ms | 28.6 ms |
+| Fast 4G (1,012,500 B/s, 165 ms) | 3,285.1 ms | 2,919.5 ms | 365.6 ms |
+| Slow 4G (180,000 B/s, 562.5 ms) | 16,177.1 ms | 14,250.7 ms | 1,926.4 ms |
+
+On Slow 4G, the last resource arrives at median 16,043.4 / 14,135.4 ms. The
+remaining loader work is 42.7 / 38.9 ms; the first `Bash.execute('echo hi')`
+is 56.9 / 46.5 ms. Resource transfer dominates both artifacts. These are
+component medians; do not add independently computed medians as an exact sum.
+
+These numbers are package-only, not brush-web's navigation path: no Ghostty,
+framework, worker, or other shared assets. CPU throttling covers this page's
+interpreter, but does not model a physical phone. Contexts have cold HTTP caches;
+the browser process can share compiled-code caches. No warm-visit claim is made.
+The development host also ran repository checks: timings are exploratory, while
+byte counts and semantic gates are deterministic. The selected bundle crossed
+below 15 seconds here, but an adopter must retain an appropriate startup budget
+for their larger application and slower networks.
+
+## Runtime tradeoff
+
+One warmup, seven measured fresh-instance executions; serial exploratory samples
+on the same development host, Node 26.5.0. Not a clean-host performance baseline.
+
+| Workload | Original median | Selected median | Rejected z median |
+|---|---:|---:|---:|
+| 20,000 arithmetic iterations | 44.24 ms | 49.49 ms | 66.18 ms |
+| 1,000 command substitutions | 6.28 ms | 6.15 ms | 7.81 ms |
+| jq map/sum over 1,000 values | 2.46 ms | 2.28 ms | 3.07 ms |
+| sed/sort/wc over 10,000 lines | 9.83 ms | 10.26 ms | 11.97 ms |
+
+The selected profile trades about 12% arithmetic throughput in this sample for
+14% less download. No claim of universal throughput improvement. The gain is a
+real first-visit improvement with all package behavior retained.
+
+## Gates and publication boundary
+
+CI and npm publication run identical checks: all browser package tests, 24-case
+GNU Bash parity, and raw/gzip/Brotli budgets (8,000,000 / 3,100,000 / 2,220,000
+bytes, about 4-5% measured headroom). There is no noisy wall-clock CI threshold.
+The original artifact fails these budgets; the shipping artifact passes.
+`build-info.json` and npm-archive tests make exact source/profile/hash verifiable.
+
+No npm release was performed by this benchmark change. On October 15,
+`just check-nightly` failed because one of the last three nightly runs had a red
+Advisory Audit ([run 38679252914](https://github.com/everruns/bashkit/actions/runs/38679252914));
+all three fuzz runs were green. The release contract prohibits publication until
+that gate is green. A merged code change is distinct from a repaired npm package.
diff --git a/crates/bashkit-wasm/bench/runtime.mjs b/crates/bashkit-wasm/bench/runtime.mjs
new file mode 100644
index 000000000..68f640412
--- /dev/null
+++ b/crates/bashkit-wasm/bench/runtime.mjs
@@ -0,0 +1,31 @@
+// Representative commands guard against buying transfer savings with unusable execution.
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { pathToFileURL } from "node:url";
+const pkg = resolve(process.argv[2]);
+const { initBashkit, Bash } = await import(pathToFileURL(resolve(pkg, "index.js")));
+await initBashkit(readFileSync(resolve(pkg, "bashkit_wasm_bg.wasm")));
+const cases = {
+  arithmetic: "n=0; for ((i=0;i<20000;i++)); do ((n+=i)); done; echo $n",
+  substitutions: 'for ((i=0;i<1000;i++)); do a=$(echo hi); done; echo "$a"',
+  jq: "seq 1000 | jq -s 'map(. * 2) | add'",
+  text: "seq 10000 | sed 's/1/x/g' | sort | wc -l",
+};
+const results = {};
+for (const [name, script] of Object.entries(cases)) {
+  const samples = [];
+  let output;
+  for (let i = 0; i < 8; i++) {
+    const bash = new Bash({ profile: "interactive", maxCommands: 1000000, maxLoopIterations: 100000 });
+    const start = performance.now();
+    const result = await bash.execute(script);
+    const ms = performance.now() - start;
+    if (result.exitCode !== 0) throw new Error(`${name}: ${result.stderr}`);
+    if (output !== undefined && output !== result.stdout) throw new Error("unstable output");
+    output = result.stdout;
+    if (i) samples.push(ms);
+    result.free(); bash.free();
+  }
+  results[name] = { stdout: output, samples, median: [...samples].sort((a, b) => a - b)[3] };
+}
+console.log(JSON.stringify({ node: process.version, package: pkg, results }, null, 2));
diff --git a/crates/bashkit-wasm/bench/size-budget.json b/crates/bashkit-wasm/bench/size-budget.json
new file mode 100644
index 000000000..8eaa130ac
--- /dev/null
+++ b/crates/bashkit-wasm/bench/size-budget.json
@@ -0,0 +1,5 @@
+{
+  "raw": 8000000,
+  "gzip": 3100000,
+  "brotli": 2220000
+}
diff --git a/crates/bashkit-wasm/package.json b/crates/bashkit-wasm/package.json
index 8c903cd2e..5cd08712b 100644
--- a/crates/bashkit-wasm/package.json
+++ b/crates/bashkit-wasm/package.json
@@ -11,7 +11,8 @@
       "types": "./index.d.ts",
       "default": "./index.js"
     },
-    "./bashkit_wasm_bg.wasm": "./bashkit_wasm_bg.wasm"
+    "./bashkit_wasm_bg.wasm": "./bashkit_wasm_bg.wasm",
+    "./build-info.json": "./build-info.json"
   },
   "sideEffects": [
     "./index.js",
@@ -24,6 +25,7 @@
     "bashkit_wasm.d.ts",
     "bashkit_wasm_bg.wasm",
     "bashkit_wasm_bg.wasm.d.ts",
+    "build-info.json",
     "README.md"
   ],
   "license": "MIT",
diff --git a/crates/bashkit-wasm/scripts/build-info.mjs b/crates/bashkit-wasm/scripts/build-info.mjs
new file mode 100644
index 000000000..264d049e5
--- /dev/null
+++ b/crates/bashkit-wasm/scripts/build-info.mjs
@@ -0,0 +1,35 @@
+// Publish the exact source/build identity with the bundle; version alone stays
+// unchanged between releases and cannot identify a source-tree build.
+import { execFileSync } from "node:child_process";
+import { createHash } from "node:crypto";
+import { readFileSync, writeFileSync, realpathSync } from "node:fs";
+import { resolve } from "node:path";
+const [pkg, profile, features, wasmOpt] = process.argv.slice(2);
+const workspace = realpathSync(new URL("../../../", import.meta.url));
+const command = (name, args) => execFileSync(name, args, {
+  cwd: workspace, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
+}).trim();
+let sourceRevision = null;
+let sourceDirty = null;
+try {
+  if (realpathSync(command("git", ["rev-parse", "--show-toplevel"])) !== workspace) {
+    throw new Error("source archive inside another repository");
+  }
+  sourceRevision = command("git", ["rev-parse", "HEAD"]);
+  sourceDirty = command("git", ["status", "--porcelain"]).length > 0;
+} catch {
+  // Source archives have no Git identity. Record absence instead of guessing
+  // from the package version; CI/release checkouts always have both fields.
+}
+const info = {
+  version: JSON.parse(readFileSync(resolve(pkg, "package.json"))).version,
+  sourceRevision,
+  sourceDirty,
+  profile,
+  features: features.split(",").filter(Boolean).sort(),
+  rustc: command("rustc", ["--version"]),
+  wasmBindgen: command("wasm-bindgen", ["--version"]),
+  wasmOpt,
+  wasmSha256: createHash("sha256").update(readFileSync(resolve(pkg, "bashkit_wasm_bg.wasm"))).digest("hex"),
+};
+writeFileSync(resolve(pkg, "build-info.json"), JSON.stringify(info, null, 2) + "\n");
diff --git a/crates/bashkit-wasm/scripts/build.sh b/crates/bashkit-wasm/scripts/build.sh
index cf0d04875..b4a462775 100755
--- a/crates/bashkit-wasm/scripts/build.sh
+++ b/crates/bashkit-wasm/scripts/build.sh
@@ -12,23 +12,24 @@ CRATE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
 WORKSPACE_ROOT="$(cd "$CRATE_DIR/../.." && pwd)"
 OUT_DIR="$CRATE_DIR/pkg"
 TARGET=wasm32-unknown-unknown
-PROFILE="${1:-release}"
+PROFILE="${1:-wasm-release}"
 # Cargo features, comma-separated. The npm package ships `terminal` (the
 # `Terminal` class, ~60 KB gzipped); set BASHKIT_WASM_FEATURES="" to drop it.
 FEATURES="${BASHKIT_WASM_FEATURES-terminal}"
 
+cd "$WORKSPACE_ROOT"
+# Keep `release` as the workflow-facing spelling; both CI and publication use
+# the same dedicated profile. Reject typos instead of silently shipping debug.
+case "$PROFILE" in
+  release|wasm-release) PROFILE=wasm-release; TARGET_SUBDIR=wasm-release ;;
+  dev|debug) PROFILE=dev; TARGET_SUBDIR=debug ;;
+  *) echo "unsupported profile: $PROFILE" >&2; exit 1 ;;
+esac
 echo "==> cargo build ($PROFILE) for $TARGET"
-if [ "$PROFILE" = "release" ]; then
-  CARGO_PROFILE_FLAG="--release"
-  TARGET_SUBDIR="release"
-else
-  CARGO_PROFILE_FLAG=""
-  TARGET_SUBDIR="debug"
-fi
 # shellcheck disable=SC2086
-cargo build -p bashkit-wasm --target "$TARGET" $CARGO_PROFILE_FLAG ${FEATURES:+--features "$FEATURES"}
+cargo build --locked -p bashkit-wasm --target "$TARGET" --profile "$PROFILE" ${FEATURES:+--features "$FEATURES"}
 
-WASM_IN="$WORKSPACE_ROOT/target/$TARGET/$TARGET_SUBDIR/bashkit_wasm.wasm"
+WASM_IN="${CARGO_TARGET_DIR:-$WORKSPACE_ROOT/target}/$TARGET/$TARGET_SUBDIR/bashkit_wasm.wasm"
 
 echo "==> wasm-bindgen --target web"
 rm -rf "$OUT_DIR"
@@ -40,6 +41,7 @@ wasm-bindgen "$WASM_IN" \
   --omit-default-module-path
 
 if command -v wasm-opt >/dev/null 2>&1; then
+  WASM_OPT_VERSION="$(wasm-opt --version)"
   # Rust emits memory.copy, saturating float conversions, and sign extension;
   # enable those stable features without accepting unrelated proposals.
   echo "==> wasm-opt -Oz (Rust WebAssembly features enabled)"
@@ -49,6 +51,7 @@ if command -v wasm-opt >/dev/null 2>&1; then
     --enable-sign-ext \
     "$OUT_DIR/bashkit_wasm_bg.wasm" -o "$OUT_DIR/bashkit_wasm_bg.wasm"
 else
+  WASM_OPT_VERSION=none
   echo "==> wasm-opt not found; skipping size optimization"
 fi
 
@@ -57,6 +60,7 @@ cp "$CRATE_DIR/js/index.js" "$OUT_DIR/index.js"
 cp "$CRATE_DIR/js/index.d.ts" "$OUT_DIR/index.d.ts"
 cp "$CRATE_DIR/package.json" "$OUT_DIR/package.json"
 cp "$CRATE_DIR/README.md" "$OUT_DIR/README.md"
+node "$CRATE_DIR/scripts/build-info.mjs" "$OUT_DIR" "$PROFILE" "$FEATURES" "$WASM_OPT_VERSION"
 
 echo "==> built $OUT_DIR"
 ls -lh "$OUT_DIR"
diff --git a/knowledge/operations/performance-results.md b/knowledge/operations/performance-results.md
index 8487154bf..eda55efff 100644
--- a/knowledge/operations/performance-results.md
+++ b/knowledge/operations/performance-results.md
@@ -100,3 +100,14 @@ most misses). `/benches` renders both after the gap telemetry panels. See
 
 When changing result schemas, update the transformer and this spec in the same
 PR. Do not hand-edit `performance-timeline.json` except by running the script.
+
+## Browser package experiments
+
+Browser transfer/startup reports use
+`crates/bashkit-wasm/bench/results/wasm-*.{json,md}`. The scripts emit JSON to
+stdout for explicit capture; browser measurements are exploratory and do not
+refresh `/benches` or use the native/ Criterion schemas. They record exact
+artifact hashes and compressed transfer bytes alongside cold Chromium timings.
+CI gates deterministic size budgets and a bounded compatibility sample, not
+noisy startup time. See [Browser Package](../runtimes/browser-package.md) and the
+[methodology](../../crates/bashkit-wasm/bench/README.md).
diff --git a/knowledge/operations/release-process.md b/knowledge/operations/release-process.md
index ccf806fad..75a93e510 100644
--- a/knowledge/operations/release-process.md
+++ b/knowledge/operations/release-process.md
@@ -309,6 +309,14 @@ integration suite, and publishes `@everruns/bashkit-wasm` to npm with
 provenance. Uses the same `NPM_TOKEN` as `publish-js.yml`; the package version
 is synced manually from the workspace version during release preparation.
 
+CI and publication both use the `wasm-release` profile and run the package's
+GNU Bash compatibility sample, compressed-size budgets, and npm archive tests.
+The archive includes `build-info.json` with source commit and the shipped WASM
+hash; inspect those fields after publication as well as npm version/dist-tags.
+A current source checkout can still say `0.18.2` while containing hundreds of
+commits absent from the published `0.18.2`, so version equality does not prove
+artifact parity. See [Browser Package](../runtimes/browser-package.md).
+
 ### c-api-binaries.yml
 
 Dispatched by `release.yml` from the verified release tag. Builds and attaches
diff --git a/knowledge/runtimes/browser-package.md b/knowledge/runtimes/browser-package.md
index bd7a615ad..131ff7cc7 100644
--- a/knowledge/runtimes/browser-package.md
+++ b/knowledge/runtimes/browser-package.md
@@ -186,7 +186,7 @@ core already gates off under `cfg(target_family = "wasm")` (see
   loads from a CDN, a bundler, or a plain `