diff --git a/.github/workflows/reusable-homebrew-bottle-publish.yml b/.github/workflows/reusable-homebrew-bottle-publish.yml index 82b19c9133..1dd60641fe 100644 --- a/.github/workflows/reusable-homebrew-bottle-publish.yml +++ b/.github/workflows/reusable-homebrew-bottle-publish.yml @@ -340,75 +340,90 @@ jobs: bash scripts/dev-shell.sh bash "$sidecar_script" browser_gallery_root="" - if [ "$KANDELO_HOMEBREW_FORMULA" = "hello" ] && [ "$KANDELO_HOMEBREW_ARCH" = "wasm32" ]; then - browser_vfs_root="$RUNNER_TEMP/homebrew-browser-vfs" + if [ "$KANDELO_HOMEBREW_ARCH" = "wasm32" ]; then + browser_result_dir="$RUNNER_TEMP/homebrew-browser-smoke" browser_gallery_root="$RUNNER_TEMP/homebrew-browser-gallery" - browser_public_dir="$GITHUB_WORKSPACE/kandelo/apps/browser-demos/public/__kandelo-smoke" browser_port="${KANDELO_PLAYWRIGHT_PORT:-5401}" - browser_vfs="$browser_vfs_root/homebrew-hello.vfs.zst" - browser_report="$browser_vfs_root/homebrew-hello.vfs-report.json" - browser_url="http://127.0.0.1:${browser_port}/__kandelo-smoke/homebrew-hello.vfs.zst" - mkdir -p "$browser_vfs_root" "$browser_gallery_root" "$browser_public_dir" - - bash scripts/dev-shell.sh npx tsx images/vfs/scripts/build-homebrew-vfs-image.ts \ - --metadata "$sidecar_root/Kandelo/metadata.json" \ - --tap-root "$sidecar_root" \ - --package hello \ - --arch wasm32 \ - --runtime node \ - --out "$browser_vfs" \ - --report "$browser_report" \ - --bottle-cache "$RUNNER_TEMP/homebrew-bottle-cache" \ - --write-profile - cp "$browser_vfs" "$browser_public_dir/homebrew-hello.vfs.zst" + mkdir -p "$browser_result_dir" "$browser_gallery_root" ( cd apps/browser-demos npx playwright install chromium --with-deps - KANDELO_PLAYWRIGHT_PORT="$browser_port" \ - KANDELO_BROWSER_HELLO_VFS_URL="$browser_url" \ - npx playwright test test/kandelo-homebrew.spec.ts \ - --project=chromium \ - --grep "Homebrew hello VFS image boots" ) - export KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS="success" - export KANDELO_HOMEBREW_VFS_IMAGE="$browser_vfs" - export KANDELO_HOMEBREW_VFS_REPORT="$browser_report" - export KANDELO_HOMEBREW_GALLERY_ROOT="$browser_gallery_root" - export KANDELO_HOMEBREW_BROWSER_SMOKE_URL="$browser_url" - export KANDELO_HOMEBREW_BROWSER_SMOKE_COMMAND="/home/linuxbrew/.linuxbrew/bin/hello --version" + set +e + KANDELO_PLAYWRIGHT_PORT="$browser_port" \ + bash scripts/dev-shell.sh npx tsx scripts/homebrew-package-browser-smoke.ts \ + --tap-root "$sidecar_root" \ + --formula "$KANDELO_HOMEBREW_FORMULA" \ + --arch wasm32 \ + --result-dir "$browser_result_dir" \ + --bottle-cache "$RUNNER_TEMP/homebrew-bottle-cache" \ + --port "$browser_port" \ + --run-id "gha-${KANDELO_HOMEBREW_FORMULA}-${KANDELO_HOMEBREW_ARCH}-${GITHUB_RUN_ID}" + browser_smoke_exit=$? + set -e + + if [ ! -f "$browser_result_dir/summary.json" ]; then + echo "::error::browser smoke did not produce $browser_result_dir/summary.json" + exit 1 + fi + + export KANDELO_HOMEBREW_BROWSER_SMOKE_SUMMARY="$browser_result_dir/summary.json" rm -rf "$sidecar_root" mkdir -p "$sidecar_root" bash scripts/dev-shell.sh bash "$sidecar_script" - bash scripts/homebrew-create-browser-gallery.sh \ - --metadata "$sidecar_root/Kandelo/metadata.json" \ - --image "$browser_vfs" \ - --report "$browser_report" \ - --out "$browser_gallery_root" \ - --formula hello \ - --title "GNU hello Homebrew VFS" \ - --description "GNU hello poured from the published Automattic/kandelo-homebrew bottle into a browser-smoked Kandelo VFS image." - node scripts/validate-software-gallery.mjs \ - --gallery "$browser_gallery_root/gallery.json" \ - --index "$browser_gallery_root/index.toml" - - if [ "$KANDELO_HOMEBREW_DRY_RUN" != "true" ]; then - if ! gh release view "$KANDELO_HOMEBREW_RELEASE_TAG" --repo "$KANDELO_HOMEBREW_TAP_REPOSITORY" >/dev/null 2>&1; then - gh release create "$KANDELO_HOMEBREW_RELEASE_TAG" \ - --repo "$KANDELO_HOMEBREW_TAP_REPOSITORY" \ - --target "$(git -C "$GITHUB_WORKSPACE/tap" rev-parse HEAD)" \ - --title "$KANDELO_HOMEBREW_RELEASE_TAG" \ - --notes "Kandelo Homebrew bottle sidecars and browser gallery assets." + if [ "$browser_smoke_exit" -ne 0 ]; then + echo "::warning::browser smoke exited $browser_smoke_exit; final sidecars record the browser outcome from $browser_result_dir/summary.json" + fi + + if jq -e \ + --arg formula "$KANDELO_HOMEBREW_FORMULA" \ + --arg arch "$KANDELO_HOMEBREW_ARCH" \ + '.packages[] | select(.formula == $formula and .arch == $arch and .status == "success")' \ + "$browser_result_dir/summary.json" >/dev/null; then + browser_vfs="$(jq -r \ + --arg formula "$KANDELO_HOMEBREW_FORMULA" \ + --arg arch "$KANDELO_HOMEBREW_ARCH" \ + '.packages[] | select(.formula == $formula and .arch == $arch) | .vfs_image' \ + "$browser_result_dir/summary.json")" + browser_report="$(jq -r \ + --arg formula "$KANDELO_HOMEBREW_FORMULA" \ + --arg arch "$KANDELO_HOMEBREW_ARCH" \ + '.packages[] | select(.formula == $formula and .arch == $arch) | .vfs_report' \ + "$browser_result_dir/summary.json")" + + if [ "$KANDELO_HOMEBREW_FORMULA" = "hello" ]; then + bash scripts/homebrew-create-browser-gallery.sh \ + --metadata "$sidecar_root/Kandelo/metadata.json" \ + --image "$browser_vfs" \ + --report "$browser_report" \ + --out "$browser_gallery_root" \ + --formula hello \ + --title "GNU hello Homebrew VFS" \ + --description "GNU hello poured from the published Automattic/kandelo-homebrew bottle into a browser-smoked Kandelo VFS image." + node scripts/validate-software-gallery.mjs \ + --gallery "$browser_gallery_root/gallery.json" \ + --index "$browser_gallery_root/index.toml" + + if [ "$KANDELO_HOMEBREW_DRY_RUN" != "true" ]; then + if ! gh release view "$KANDELO_HOMEBREW_RELEASE_TAG" --repo "$KANDELO_HOMEBREW_TAP_REPOSITORY" >/dev/null 2>&1; then + gh release create "$KANDELO_HOMEBREW_RELEASE_TAG" \ + --repo "$KANDELO_HOMEBREW_TAP_REPOSITORY" \ + --target "$(git -C "$GITHUB_WORKSPACE/tap" rev-parse HEAD)" \ + --title "$KANDELO_HOMEBREW_RELEASE_TAG" \ + --notes "Kandelo Homebrew bottle sidecars and browser gallery assets." + fi + gh release upload "$KANDELO_HOMEBREW_RELEASE_TAG" \ + "$browser_gallery_root/gallery.json" \ + "$browser_gallery_root/index.toml" \ + "$browser_gallery_root"/*.tar.zst \ + --repo "$KANDELO_HOMEBREW_TAP_REPOSITORY" \ + --clobber + fi fi - gh release upload "$KANDELO_HOMEBREW_RELEASE_TAG" \ - "$browser_gallery_root/gallery.json" \ - "$browser_gallery_root/index.toml" \ - "$browser_gallery_root"/*.tar.zst \ - --repo "$KANDELO_HOMEBREW_TAP_REPOSITORY" \ - --clobber fi fi @@ -460,6 +475,7 @@ jobs: path: | ${{ runner.temp }}/homebrew-bottle/** ${{ runner.temp }}/homebrew-browser-gallery/** + ${{ runner.temp }}/homebrew-browser-smoke/** ${{ runner.temp }}/homebrew-browser-vfs/** ${{ runner.temp }}/homebrew-sidecars/** ${{ runner.temp }}/homebrew-*.txt diff --git a/apps/browser-demos/pages/homebrew-smoke/index.html b/apps/browser-demos/pages/homebrew-smoke/index.html new file mode 100644 index 0000000000..b560e65ff2 --- /dev/null +++ b/apps/browser-demos/pages/homebrew-smoke/index.html @@ -0,0 +1,35 @@ + + + + + + Kandelo Homebrew Browser Smoke + + + +
Loading
+

+    
+  
+
diff --git a/apps/browser-demos/pages/homebrew-smoke/main.ts b/apps/browser-demos/pages/homebrew-smoke/main.ts
new file mode 100644
index 0000000000..4b63efbddd
--- /dev/null
+++ b/apps/browser-demos/pages/homebrew-smoke/main.ts
@@ -0,0 +1,126 @@
+import { BrowserKernel } from "@host/browser-kernel-host";
+import { ABI_VERSION } from "@host/generated/abi";
+import { MemoryFileSystem } from "@host/vfs/memory-fs";
+import kernelWasmUrl from "@kernel-wasm?url";
+
+interface HomebrewSmokeRequest {
+  vfsUrl: string;
+  argv: string[];
+  timeoutMs?: number;
+  cwd?: string;
+  env?: string[];
+}
+
+interface HomebrewSmokeResult {
+  exitCode: number;
+  stdout: string;
+  stderr: string;
+  combined: string;
+  durationMs: number;
+}
+
+declare global {
+  interface Window {
+    __homebrewSmokeReady: boolean;
+    __runHomebrewSmoke: (request: HomebrewSmokeRequest) => Promise;
+  }
+}
+
+const statusEl = document.getElementById("status")!;
+const logEl = document.getElementById("log")!;
+const decoder = new TextDecoder();
+
+let kernelBytes: ArrayBuffer | null = null;
+
+function appendLog(text: string): void {
+  logEl.textContent += text;
+}
+
+async function fetchBytes(url: string, label: string): Promise {
+  const response = await fetch(url);
+  if (!response.ok) {
+    throw new Error(`${label} fetch failed: ${response.status} ${response.statusText}`);
+  }
+  return response.arrayBuffer();
+}
+
+function timeoutAfter(ms: number): Promise {
+  return new Promise((_, reject) => {
+    window.setTimeout(() => reject(new Error("TIMEOUT")), ms);
+  });
+}
+
+async function runHomebrewSmoke(request: HomebrewSmokeRequest): Promise {
+  if (!kernelBytes) throw new Error("kernel wasm is not loaded");
+  if (!request.vfsUrl) throw new Error("vfsUrl is required");
+  if (!Array.isArray(request.argv) || request.argv.length === 0) {
+    throw new Error("argv must contain at least argv[0]");
+  }
+
+  const start = performance.now();
+  let stdout = "";
+  let stderr = "";
+  const vfsBytes = new Uint8Array(await fetchBytes(request.vfsUrl, "Homebrew VFS"));
+  MemoryFileSystem.assertImageKernelAbi(vfsBytes, ABI_VERSION, "Homebrew smoke VFS");
+
+  const kernel = new BrowserKernel({
+    kernelOwnedFs: true,
+    onStdout: (data) => {
+      const text = decoder.decode(data);
+      stdout += text;
+      appendLog(text);
+    },
+    onStderr: (data) => {
+      const text = decoder.decode(data);
+      stderr += text;
+      appendLog(text);
+    },
+  });
+
+  try {
+    const { exit } = await kernel.boot({
+      kernelWasm: kernelBytes,
+      vfsImage: vfsBytes,
+      argv: request.argv,
+      cwd: request.cwd ?? "/",
+      env: request.env ?? [
+        "HOME=/tmp",
+        "TMPDIR=/tmp",
+        "TERM=xterm-256color",
+        "LANG=en_US.UTF-8",
+        "PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/bin:/usr/bin:/bin",
+      ],
+      uid: 0,
+      gid: 0,
+      stdin: new Uint8Array(0),
+    });
+    const exitCode = await Promise.race([
+      exit,
+      timeoutAfter(request.timeoutMs ?? 180_000),
+    ]);
+    return {
+      exitCode,
+      stdout,
+      stderr,
+      combined: `${stdout}${stderr}`,
+      durationMs: Math.round(performance.now() - start),
+    };
+  } finally {
+    await kernel.destroy().catch(() => {});
+  }
+}
+
+async function init(): Promise {
+  kernelBytes = await fetchBytes(kernelWasmUrl, "kernel.wasm");
+  window.__runHomebrewSmoke = runHomebrewSmoke;
+  window.__homebrewSmokeReady = true;
+  statusEl.textContent = "Ready";
+}
+
+window.__homebrewSmokeReady = false;
+init().catch((err) => {
+  const message = err instanceof Error ? err.message : String(err);
+  statusEl.textContent = `Error: ${message}`;
+  appendLog(`${message}\n`);
+  console.error("Homebrew smoke init failed:", err);
+});
diff --git a/apps/browser-demos/vite.config.ts b/apps/browser-demos/vite.config.ts
index f517aaba2f..705519dc63 100644
--- a/apps/browser-demos/vite.config.ts
+++ b/apps/browser-demos/vite.config.ts
@@ -403,6 +403,7 @@ const defaultDemoInputs = {
 
 const demoInputs = {
   ...defaultDemoInputs,
+  "homebrew-smoke": path.resolve(__dirname, "pages/homebrew-smoke/index.html"),
   "sqlite-test": path.resolve(__dirname, "pages/sqlite-test/index.html"),
   // The perl, python, ruby, erlang, texlive, and redis package entries
   // are not bundled into this static build while their slow builds
diff --git a/docs/homebrew-publishing.md b/docs/homebrew-publishing.md
index d14760066e..ef205903af 100644
--- a/docs/homebrew-publishing.md
+++ b/docs/homebrew-publishing.md
@@ -215,6 +215,28 @@ path calls `scripts/homebrew-publish-sidecars.sh --status failed` so the failed
 attempt is durable while the previous successful bottle remains selectable when
 its fallback fields are complete.
 
+The default trusted sidecar wrapper,
+`scripts/homebrew-generate-sidecars-from-env.sh`, derives non-hello link
+manifests from package kind. Program Formulae link their installed
+`bin/` into the Homebrew prefix. Library Formulae link the declared
+`[outputs]` headers, static libraries, and pkg-config files from the keg into
+the prefix. Package-specific Node and browser outcome text may be supplied via
+`KANDELO_HOMEBREW_NODE_SMOKE_COMMAND`,
+`KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS`, and
+`KANDELO_HOMEBREW_BROWSER_SMOKE_REASON`; browser compatibility is recorded
+only when `KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS=success` and the browser VFS
+smoke artifact environment is complete.
+
+Trusted publication can also supply
+`KANDELO_HOMEBREW_BROWSER_SMOKE_SUMMARY=/path/to/summary.json` from
+`scripts/homebrew-package-browser-smoke.ts`. When present, that summary is the
+authoritative browser outcome for the current formula and arch: successful
+wasm32 package entries become `runtime_support = ["node", "browser"]` and
+`browser_compatible = true`; failed or skipped entries stay Node-only and
+record the concrete browser cases, reasons, and artifact paths in the
+`browser_smoke` provenance outcome. The older manual browser-smoke environment
+variables remain available for transitional and one-off runs.
+
 ## VFS Planning And Building
 
 Homebrew-derived VFS images are built from sidecars and verified bottle bytes,
@@ -265,19 +287,67 @@ It clones or reads the tap, builds a Homebrew VFS from published sidecars, runs
 `/home/linuxbrew/.linuxbrew/bin/hello --version` through `NodeKernelHost`, and
 checks negative ABI-mismatch and missing-bottle cases.
 
+For the sqlite/bzip2/xz pilot and later non-hello package checks, use the
+generic package smoke runner against a generated tap root:
+
+```bash
+npx tsx scripts/homebrew-package-node-smoke.ts \
+  --tap-root /path/to/kandelo-homebrew \
+  --formula sqlite \
+  --formula bzip2 \
+  --formula xz \
+  --arch wasm32 \
+  --result-dir test-runs/homebrew-package-node-smoke
+```
+
+The runner builds Homebrew VFS images from sidecars, writes passed, failed,
+and skipped outcome lists, runs program package version smokes from the poured
+prefix, and compiles SQLite's `sqlite_basic.c` against the poured headers and
+static library before running the validation Wasm on Node. Dry-run bottle
+evidence remains local evidence until the trusted workflow publishes GHCR
+bottle bytes and tap sidecars.
+
 Browser compatibility requires a separate browser smoke. For the current
-`hello` path, the trusted publisher builds a precomposed wasm32 VFS image,
-serves it through the browser demo, runs Chromium Playwright against
-`apps/browser-demos/test/kandelo-homebrew.spec.ts`, and executes:
+generic path, the trusted publisher first generates candidate sidecars without
+requiring `browser_compatible=true`, then runs:
 
 ```bash
-/home/linuxbrew/.linuxbrew/bin/hello --version
+npx tsx scripts/homebrew-package-browser-smoke.ts \
+  --tap-root /path/to/kandelo-homebrew \
+  --formula sqlite \
+  --formula bzip2 \
+  --formula xz \
+  --arch wasm32 \
+  --result-dir test-runs/homebrew-package-browser-smoke
 ```
 
-Only after that smoke passes may sidecars record
+The runner materializes one Homebrew-derived VFS image per formula, serves it
+through the browser demo app, launches Chromium with the same cross-origin
+isolation requirements as the browser tests, and uses the dedicated
+`pages/homebrew-smoke/` entry to boot the candidate image through
+`BrowserKernel`. Program packages execute their poured binary directly as the
+first browser-kernel process, using explicit argv such as
+`["/home/linuxbrew/.linuxbrew/bin/bzip2", "--help"]`; sqlite compiles
+`packages/registry/sqlite/test/sqlite_basic.c` against the poured keg, injects
+that validation-only Wasm into the candidate image, and runs it the same way.
+The generic smoke entry only imports the kernel and the explicit VFS path under
+test, so unrelated browser-gallery assets and the default rootfs do not decide
+whether a Homebrew package is browser-compatible.
+
+The browser runner writes `summary.json`, `summary.md`, `current-run.json`,
+`failures.json`, and `outcome-lists/{passed,failed,skipped}-tests.tsv`.
+Skipped outcomes include reasons, such as the wasm64 browser boundary or a
+missing sqlite consumer compiler. Final sidecars are regenerated with
+`KANDELO_HOMEBREW_BROWSER_SMOKE_SUMMARY` after the smoke finishes. Only after
+that smoke passes may sidecars record
 `runtime_support = ["node", "browser"]` and `browser_compatible = true`.
 Packages without a successful browser smoke remain Node-only.
 
+The older `apps/browser-demos/test/kandelo-homebrew.spec.ts` coverage remains
+as app/gallery regression coverage for the published hello image. New
+Homebrew package browser claims should use the generic package runner instead
+of adding per-formula Playwright specs.
+
 ## Browser Gallery Assets
 
 Generate browser gallery assets only from browser-smoked wasm32 metadata:
diff --git a/docs/plans/2026-06-29-homebrew-sqlite-bzip2-xz-pilot-design.md b/docs/plans/2026-06-29-homebrew-sqlite-bzip2-xz-pilot-design.md
new file mode 100644
index 0000000000..68e3cad4c4
--- /dev/null
+++ b/docs/plans/2026-06-29-homebrew-sqlite-bzip2-xz-pilot-design.md
@@ -0,0 +1,545 @@
+# Homebrew SQLite, Bzip2, And Xz Pilot Design
+
+Date: 2026-06-29
+
+Tracked work:
+
+- `kd-1mr` - Port all current Kandelo packages to Homebrew.
+- `kd-1mr.2` - Port sqlite, bzip2, and xz Homebrew pilot.
+- Source planning evidence: `kd-5yd`, commit `b6cd51d8c`, refreshed the
+  Homebrew package inventory and selected this pilot after the trusted
+  `hello` publication path and local `zlib` wasm32/wasm64 proof.
+
+This is a design artifact for the pilot. It does not implement the Formulae,
+publish bottles, change registry package bytes, or update release metadata.
+
+## Problem Statement
+
+Kandelo's Homebrew foundation can publish and smoke the first `hello` bottle,
+and the follow-up `zlib` proof showed that a dependency-root library can be
+bottled locally for wasm32 and wasm64. The next migration step needs a small
+pilot that exercises more of the future registry-replacement model without
+starting a broad package wave.
+
+The pilot packages are deliberately mixed:
+
+- `sqlite` is the next dependency-root library. It declares wasm32 and wasm64,
+  has real downstream consumers, and has existing upstream SQLite test harnesses
+  whose status should be visible without making full upstream success the
+  default bottle gate.
+- `bzip2` and `xz` are small leaf CLI packages. They exercise program Formulae,
+  link manifests, VFS pour/link behavior, and Node/browser smoke commands
+  without the capacity risk of heavy runtimes.
+
+The design must preserve the current Homebrew direction:
+
+- Formulae become authoritative for source, dependency, build, install, and
+  `test do` behavior.
+- Kandelo sidecars remain additive metadata for ABI, cache keys, provenance,
+  VFS planning, host support, browser gallery status, and test outcomes.
+- Package failures remain visible with reasons and artifacts.
+- Node and browser hosts are both product surfaces. A missing browser result is
+  status to publish, not proof of browser support.
+
+## Non-Goals
+
+- Do not port the full dependency-root wave or the full small-CLI wave.
+- Do not delete or rename `packages/registry` in this pilot.
+- Do not revive `sqlite-cli` as a product Formula unless the pilot proves it is
+  necessary and a focused follow-up is created.
+- Do not make upstream SQLite full-suite success a default bottle publication
+  gate.
+- Do not use Formula patches or test skips to hide Kandelo runtime, libc,
+  syscall, VFS, fork-instrumentation, or host-parity defects.
+- Do not publish user-facing guest `brew install` instructions from this work.
+- Do not bump `packages/registry/*/build.toml` revisions unless package output
+  bytes legitimately change.
+
+## Users And Operator Workflows
+
+### Package Porter
+
+The porter authors or regenerates `Formula/sqlite.rb`, `Formula/bzip2.rb`, and
+`Formula/xz.rb` in `Automattic/kandelo-homebrew` or the reviewable tap fixture.
+They reuse existing package build knowledge, but the Formula DSL owns the
+final source, build, install, and test behavior. The existing registry scripts
+may be called only where they already honor the resolver-style output contract
+or after the pilot makes their side effects explicit.
+
+### Maintainer Reviewer
+
+The reviewer checks that the Formulae are normal Homebrew Formulae with the
+minimum Kandelo-specific environment wiring, that sidecars describe the bottles
+truthfully, that dry-run/local evidence is not presented as trusted
+publication, and that package failures are recorded instead of disappearing.
+
+### Trusted Publisher
+
+The trusted workflow builds each selected `(formula, arch)` entry, uploads the
+bottle to the GHCR-backed Homebrew bottle URL shape, generates sidecars and
+provenance, validates the tap payload, then publishes success or failure state.
+The workflow must keep failure reports durable without replacing last-green
+metadata.
+
+### Runtime Validator
+
+The validator materializes Homebrew bottles into VFS images and runs package
+smoke commands on both Node and browser hosts. For program packages, the smoke
+executes the installed program. For library packages, the smoke compiles or
+ships a test-only consumer and runs that consumer against the poured library.
+The test consumer is validation evidence, not a product bottle output.
+
+### Debugger
+
+When a package fails, the debugger needs enough metadata to classify the fault:
+source fetch, Formula generation, cross-compile configure answer, build,
+install/link, bottle upload, sidecar generation, VFS pour/link, Node runtime,
+browser runtime, upstream test failure, or Kandelo platform behavior.
+
+## Existing Package Facts
+
+`sqlite`:
+
+- `packages/registry/sqlite/package.toml` is a library manifest for SQLite
+  `3.49.1`, declares `arches = ["wasm32", "wasm64"]`, and outputs
+  `lib/libsqlite3.a`, `include/sqlite3.h`, `include/sqlite3ext.h`, and
+  `lib/pkgconfig/sqlite3.pc`.
+- `build-sqlite.sh` is resolver-shaped for the library path when
+  `WASM_POSIX_DEP_OUT_DIR` is set, but it also has a legacy direct-invocation
+  CLI path. Formulae should not rely on that legacy CLI path for the library
+  bottle.
+- Existing SQLite test tooling includes SQL fixture tests and official
+  testrunner wrappers for Node and browser. Those are upstream-test status
+  evidence, not the default bottle availability gate.
+
+`bzip2`:
+
+- `packages/registry/bzip2/package.toml` is a wasm32 program manifest for
+  `1.0.8`.
+- `build-bzip2.sh` is an older direct script. It writes `bin/bzip2.wasm`,
+  installs `libbz2.a` and `bzlib.h` to the repo sysroot, and registers a local
+  binary. A Formula must not silently rely on those sysroot/local-binary side
+  effects.
+- Existing Vitest coverage checks `bzip2 --version`; round-trip compression is
+  not covered there.
+
+`xz`:
+
+- `packages/registry/xz/package.toml` says version `5.6.2`, while
+  `build-xz.sh` currently defaults to `5.6.4` and downloads a `.tar.gz` from
+  the GitHub release path. This version/source mismatch is a pilot blocker
+  unless the Formula pins one source and the build path is made consistent.
+- `build-xz.sh` is an older direct script. It writes `bin/xz.wasm`, installs
+  `liblzma.a` and headers to the repo sysroot, and registers a local binary.
+  A Formula should install only the intended bottle contents into the Homebrew
+  keg.
+- Existing Vitest coverage checks `xz --version`; round-trip compression is
+  not covered there.
+
+Cross-cutting:
+
+- Current package manifests have stale `kernel_abi` values. The Homebrew
+  sidecar path must use live `ABI_VERSION` and computed `cache_key_sha`
+  evidence. The pilot should not treat stale manifest ABI fields as bottle
+  compatibility truth.
+- `scripts/homebrew-generate-sidecars-from-env.sh` is currently heavily shaped
+  around the `hello` validation path. The pilot needs a generic sidecar input
+  path for package-specific validation outcomes, or it must publish non-hello
+  failure/deferred status with a focused follow-up.
+
+## Architecture And Data Flow
+
+The pilot should use the same publication architecture as the existing
+Homebrew path:
+
+```text
+Formula/sqlite.rb, Formula/bzip2.rb, Formula/xz.rb
+      |
+      v
+trusted workflow matrix
+  scripts/homebrew-plan-matrix.sh
+      |
+      v
+scripts/homebrew-bottle-build.sh
+  brew install --build-bottle
+  brew test
+  brew bottle --json
+  brew bottle --merge
+      |
+      v
+scripts/homebrew-ghcr-upload.sh
+      |
+      v
+generic sidecar input
+  cache_key_sha, ABI, bottle URL, sha, bytes
+  formula revision, bottle rebuild
+  build/test/node/browser/upstream outcome lists
+      |
+      v
+cargo xtask homebrew-sidecars
+cargo xtask homebrew-validate
+      |
+      v
+scripts/homebrew-publish-sidecars.sh
+  success or durable failed attempt
+      |
+      v
+Homebrew VFS builder and Node/browser smoke
+```
+
+Control-flow invariants:
+
+- Formula `test do` must execute the Wasm through Kandelo, not as a host Linux
+  binary.
+- Bottle bytes, formula bottle blocks, sidecars, and provenance must be
+  generated from the same build attempt.
+- `cache_key_sha` must be computed for the Formula's Kandelo package identity
+  and target arch. A bottle with a wrong cache key is stale even if Homebrew
+  version selection would accept it.
+- `sqlite` wasm64 publication can be attempted only if the Formula and package
+  build path really honor `HOMEBREW_KANDELO_ARCH=wasm64`. `bzip2` and `xz`
+  stay wasm32 unless their manifests and build paths are intentionally expanded.
+- Browser compatibility can be recorded only after a browser smoke consumes a
+  precomposed VFS image and runs the package or package-specific consumer
+  through the normal browser host.
+- Complete upstream-test outcome artifacts are package status metadata. They
+  do not decide default bottle availability unless the implementation
+  explicitly adds such a gate for one package.
+
+## Formula Shape
+
+### Shared Formula Pattern
+
+Each Formula should:
+
+1. Read `HOMEBREW_KANDELO_ROOT`, `HOMEBREW_KANDELO_ARCH`,
+   `HOMEBREW_KANDELO_NODE`, and `HOMEBREW_KANDELO_LLVM_BIN`.
+2. Source or route through the worktree-local SDK by prepending
+   `/sdk/bin`.
+3. Set `WASM_POSIX_DEP_VERSION`, `WASM_POSIX_DEP_SOURCE_URL`,
+   `WASM_POSIX_DEP_SOURCE_SHA256`, `WASM_POSIX_DEP_OUT_DIR`,
+   `WASM_POSIX_DEP_WORK_DIR`, and `WASM_POSIX_DEP_TARGET_ARCH`.
+4. Install only the intended artifacts from the package output dir into the
+   Homebrew keg.
+5. Keep `test do` small, deterministic, and runtime-backed by Kandelo.
+
+### SQLite Formula
+
+`sqlite` should be modeled as a library Formula, not as the incomplete
+`sqlite-cli` package.
+
+Bottle contents:
+
+- `lib/libsqlite3.a`
+- `include/sqlite3.h`
+- `include/sqlite3ext.h`
+- `lib/pkgconfig/sqlite3.pc`
+
+Formula `test do` should compile `packages/registry/sqlite/test/sqlite_basic.c`
+or an equivalent inline test program against the installed keg and run the
+result through Kandelo. This proves the installed library can be consumed and
+executed without shipping a product CLI in the bottle.
+
+Sidecar link manifest should include library, header, and pkg-config paths
+under the Homebrew prefix, even though no executable link is produced. The VFS
+builder should either support library-only bottles directly or the pilot should
+record the missing library-only VFS link behavior as a blocker before claiming
+sqlite browser compatibility.
+
+### Bzip2 Formula
+
+`bzip2` should be a program Formula that installs `bin/bzip2` from the produced
+Wasm file. The pilot should decide whether library byproducts are intentionally
+part of the bottle:
+
+- Minimal program-only path: install `bin/bzip2` only. This best matches the
+  current `[[outputs]]` program manifest.
+- Expanded hybrid path: install `bin/bzip2`, `lib/libbz2.a`, and `include/bzlib.h`.
+  This requires updating the package/Formula contract and treating the library
+  outputs as deliberate bottle contents.
+
+The minimal path is recommended for this pilot unless a downstream consumer
+needs `libbz2.a` immediately.
+
+Formula `test do` should run `bzip2 --version` through Kandelo and, if feasible,
+perform a file-based round trip that avoids writing compressed bytes to a PTY:
+create an input file, compress to an output file, decompress, and compare the
+content inside the Kandelo VFS.
+
+### Xz Formula
+
+`xz` should be a program Formula that installs `bin/xz` from the produced Wasm
+file. Before publication, resolve the current package/source mismatch:
+
+- either keep package version `5.6.2` and make the build script use the
+  manifest URL and sha through the resolver-style env vars;
+- or intentionally update the package and Formula to `5.6.4` with correct
+  source URL, sha, revision reasoning, and build-output validation.
+
+The first option is safer for the pilot because it avoids changing package
+output identity beyond the Homebrew path.
+
+Formula `test do` should run `xz --version` through Kandelo and, if feasible,
+perform a file-based compress/decompress round trip similar to `bzip2`.
+
+## Sidecars And Outcome Metadata
+
+The pilot needs generic sidecar generation before it can report truthful
+package status for these packages. `scripts/homebrew-generate-sidecars-from-env.sh`
+currently emits `hello`-specific outcome text and hardcoded browser/gallery
+behavior. For this pilot, introduce or use a sidecar input file that records:
+
+- formula and arch;
+- package kind: `library` or `program`;
+- selected smoke command or test-consumer command;
+- bottle build/install result;
+- Formula test result;
+- sidecar validation result;
+- Node VFS smoke result;
+- browser VFS smoke result or skip/failure reason;
+- upstream test status for sqlite;
+- artifact paths for logs, reports, VFS builder reports, and outcome lists.
+
+The generated sidecars should preserve the existing schema invariants while
+allowing each package to report package-specific outcome lists. If the generic
+input path cannot land cleanly inside this pilot, publish the package as
+`failed`, `pending`, or `deferred` with a durable reason rather than reusing
+misleading `hello` validation text.
+
+## Node And Browser Smoke Strategy
+
+Program packages:
+
+- Build a Homebrew VFS from the selected bottle and sidecars.
+- On Node, spawn `/home/linuxbrew/.linuxbrew/bin/bzip2` or
+  `/home/linuxbrew/.linuxbrew/bin/xz` through `NodeKernelHost`.
+- On browser, boot the same style of precomposed VFS image and run the
+  executable through the browser terminal.
+- Prefer a file round-trip smoke for final compatibility status. Version-only
+  smoke may be recorded as partial if the file round trip is blocked by shell,
+  PTY, or VFS-image limitations.
+
+SQLite:
+
+- Build a Homebrew VFS from the sqlite bottle and sidecars.
+- Compile a test-only `sqlite_basic.wasm` consumer against the poured keg, or
+  include a separately built validation artifact in the smoke image with
+  provenance that names the source and compiler inputs.
+- On Node, run the consumer with `NodeKernelHost`.
+- On browser, boot a precomposed smoke image that includes the sqlite keg plus
+  the test consumer and run the consumer through the browser terminal.
+- Mark sqlite browser compatibility only if the consumer runs in the browser
+  host. A library-only bottle build does not prove browser runtime support.
+
+Negative smoke should remain part of the reusable harness:
+
+- ABI mismatch rejects before bottle fetch.
+- Missing bottle or sha mismatch rejects before image save.
+- Cache-key mismatch rejects before compatibility is recorded.
+
+## Upstream Test Status
+
+SQLite is the only pilot package with an explicit upstream-test-status goal.
+
+Existing runners:
+
+- `scripts/run-sqlite-tests.sh` runs SQL fixture files through `sqlite3.wasm`.
+- `scripts/run-sqlite-upstream-tests.sh` runs individual upstream Tcl tests.
+- `scripts/run-sqlite-official-tests.sh` runs SQLite `testrunner.tcl` on Node
+  or delegates to the browser runner.
+- `scripts/run-sqlite-project-unit-tests.sh` combines Node and browser
+  official-test runs and writes summaries.
+
+The pilot should not require the full SQLite suite to pass before bottle
+availability. It should publish upstream status with:
+
+- permutation (`veryquick`, `full`, or `all`);
+- host (`node`, `browser`, or both);
+- total, passed, failed, skipped/omitted, running, ready, timeout, and
+  incomplete counts when available;
+- complete failure list with job/test name, state, case count, error count,
+  elapsed time, and artifact path;
+- complete skipped/omitted list with reasons when the harness exposes them;
+- explicit "missing category" notes where current harnesses cannot emit a
+  required list, plus a focused follow-up before the convoy claims complete
+  upstream outcome-list support.
+
+`bzip2` and `xz` should record upstream test status as `unavailable` or
+`deferred` with a reason if this pilot only runs smoke tests. Do not imply that
+version/round-trip smoke is an upstream full-test result.
+
+## Implementation Sequence
+
+1. Confirm the worktree is clean and preflighted for `kd-1mr.2`.
+2. Bring the worktree to the intended Homebrew foundation commit if the branch
+   is missing required `hello`, VFS builder, or sidecar work.
+3. Add or update Formulae in the tap fixture or tap worktree for `sqlite`,
+   `bzip2`, and `xz`.
+4. Make the package build paths Formula-safe:
+   - `sqlite`: use the resolver-style library output path and avoid the legacy
+     CLI side path.
+   - `bzip2`: prevent repo sysroot/local-binary side effects from being part
+     of Formula installation, or isolate them behind `WASM_POSIX_DEP_OUT_DIR`.
+   - `xz`: resolve the 5.6.2 vs 5.6.4 source mismatch before building.
+5. Generalize sidecar input so package-specific validation and outcome lists
+   are truthful for non-hello packages.
+6. Build local dry-run bottles for the selected arches:
+   - `sqlite`: wasm32 and wasm64 if the build path supports both.
+   - `bzip2`: wasm32.
+   - `xz`: wasm32.
+7. Validate generated sidecars and provenance with `cargo xtask
+   homebrew-validate`.
+8. Materialize VFS images from the dry-run sidecars and run Node smokes.
+9. Run browser smokes or record explicit host-failure status with artifact
+   paths and next action.
+10. Run SQLite upstream-test status jobs with durable outcome lists.
+11. If local dry-run evidence is satisfactory, run trusted publication against
+   `Automattic/kandelo-homebrew`. Keep local dry-run and trusted GHCR/tap
+   evidence separate in bead notes and sidecars.
+12. Create focused follow-up beads only for evidence-backed blockers or for the
+   next specific package wave. Do not create broad waves from assumptions.
+
+## Test And Documentation Plan
+
+Required local checks for implementation work:
+
+- `bash scripts/test-homebrew-publish-workflow.sh`
+- `scripts/verify-homebrew-kandelo-platform-tags.sh`
+- Homebrew dry-run bottle build for every selected `(formula, arch)`
+- `cargo xtask homebrew-validate --tap-root `
+- VFS builder tests affected by sidecar/link behavior:
+  `cd host && npx vitest run test/homebrew-vfs-planner.test.ts test/homebrew-vfs-builder.test.ts test/homebrew-vfs-fetch.test.ts`
+- Package-specific Node smoke artifacts for sqlite, bzip2, and xz
+- Package-specific browser smoke artifacts or explicit failure/skip status
+- SQLite upstream status command, preferably:
+  `scripts/run-sqlite-project-unit-tests.sh --host both --permutation veryquick`
+  for pilot evidence before considering a broader `full` run
+
+Required outcome artifacts:
+
+- Build/install pass/fail list for each Formula/arch.
+- Sidecar validation pass/fail list.
+- Node smoke passed, failed, and skipped lists.
+- Browser smoke passed, failed, and skipped lists with reasons.
+- SQLite upstream passed, failed, skipped/omitted, and incomplete lists, or
+  documented missing categories plus follow-up.
+- Bottle build logs, bottle JSON, bottle archive sha/byte report, sidecar
+  payload, VFS builder report, and browser smoke trace or screenshot artifacts.
+
+Reference docs after implementation:
+
+- Update `docs/homebrew-publishing.md` if the pilot generalizes non-hello
+  sidecar input, non-hello browser smoke, library Formula support, or outcome
+  metadata semantics.
+- Update `docs/package-management.md` only if the package/cache/revision
+  contract changes. Do not update it for tap-only Formula wording.
+- Update package-specific docs or notes if `bzip2`/`xz` build scripts are
+  converted from direct sysroot writers to resolver-style outputs.
+
+Because this design artifact is docs-only, it requires only lightweight docs
+verification. Runtime and package validation belongs to the implementation
+work.
+
+## Alternatives Considered
+
+### Port sqlite alone
+
+Rejected for the pilot. SQLite is the important dependency-root case, but it
+does not exercise program link manifests, executable smoke commands, or small
+CLI publication capacity. Adding `bzip2` and `xz` gives useful signal without
+starting the full small-CLI wave.
+
+### Port only small CLI packages
+
+Rejected. The migration needs another dependency-root proof after `zlib`,
+especially one with upstream-test status and wasm64 considerations.
+
+### Treat sqlite as a CLI Formula
+
+Rejected for this pilot. The current publishable registry package is the
+library. `sqlite-cli` is manifest-incomplete and should be revived or removed
+through a separate decision.
+
+### Reuse current bzip2/xz scripts unchanged
+
+Rejected as the default. They write into the repo sysroot and local-binaries,
+which is not a clean Homebrew keg contract. The pilot may reuse their compile
+knowledge, but Formula installation must be isolated to declared outputs.
+
+### Mark browser support from Node VFS smoke
+
+Rejected. Browser is a peer host. Node VFS materialization is necessary but not
+sufficient for browser compatibility.
+
+### Gate sqlite bottle availability on full upstream tests
+
+Rejected. Upstream test status must be visible, but full-suite success is not
+the default bottle gate. A failing upstream subset should publish a package
+status and failure list without hiding a buildable/installable bottle.
+
+## Risks And Mitigations
+
+Registry and Formula divergence:
+
+- Risk: Formulae and `packages/registry` scripts become two independent
+  recipes during the bridge period.
+- Mitigation: keep Formulae authoritative for Homebrew behavior, isolate any
+  registry-script reuse behind explicit env vars, and record bridge deletion
+  criteria in the broader replacement docs.
+
+Misleading sidecars:
+
+- Risk: reusing `hello`-specific sidecar text for non-hello packages would
+  claim validation that did not run.
+- Mitigation: add generic sidecar outcome input before success publication, or
+  publish failure/deferred state with a blocker.
+
+Library smoke ambiguity:
+
+- Risk: a sqlite bottle can build and pour while no runtime consumer has proven
+  the library works.
+- Mitigation: require a test-only consumer on Node and browser before marking
+  runtime compatibility.
+
+Version/source drift:
+
+- Risk: `xz` package metadata and build script fetch different versions.
+- Mitigation: resolve source/version identity before any bottle evidence is
+  accepted.
+
+Browser parity drift:
+
+- Risk: Node smokes pass but browser VFS boot, terminal execution,
+  SharedArrayBuffer setup, or browser fetch path fails.
+- Mitigation: record browser smoke separately and keep host-specific failures
+  visible.
+
+Outcome-list gaps:
+
+- Risk: existing test runners emit summaries and failures but not complete
+  passed/skipped lists.
+- Mitigation: add exporters where reasonable; otherwise record the missing
+  category and create a focused follow-up before claiming convoy completion.
+
+Unnecessary rebuild churn:
+
+- Risk: implementers bump `build.toml.revision` to force Homebrew rebuilds.
+- Mitigation: use Homebrew formula revision or bottle rebuild for bottle
+  selection. Bump Kandelo package revision only when package archive output
+  bytes legitimately change.
+
+## Open Questions
+
+- Should `bzip2` and `xz` remain program-only bottles, or should their library
+  byproducts become deliberate Homebrew bottle outputs?
+- Does the VFS builder already handle library-only bottles well enough for
+  sqlite browser smoke, or does the pilot need a small extension for
+  validation-only consumers?
+- Should sqlite wasm64 be trusted-published in this pilot, or should wasm64
+  stay local evidence until the sidecar/browser story is proven on wasm32?
+- What exact generic sidecar input format should replace the current
+  `hello`-specific outcome generation?
+- Which SQLite upstream-test permutation is the right first durable status:
+  `veryquick`, `full`, or both with different publication meanings?
+- Should `sqlite-cli` be revived as a separate Formula after the library pilot,
+  or removed from the accepted package set before registry deletion?
diff --git a/docs/plans/2026-06-30-homebrew-generic-browser-smoke-design.md b/docs/plans/2026-06-30-homebrew-generic-browser-smoke-design.md
new file mode 100644
index 0000000000..77d4d1a657
--- /dev/null
+++ b/docs/plans/2026-06-30-homebrew-generic-browser-smoke-design.md
@@ -0,0 +1,520 @@
+# Generic Homebrew Browser Smoke Design
+
+Date: 2026-06-30
+
+Tracked work:
+
+- `kd-1mr` - Port all current Kandelo packages to Homebrew.
+- `kd-1mr.2` - Port sqlite, bzip2, and xz Homebrew pilot.
+- `kd-1mr.2.1` - Add generic browser smoke for non-hello Homebrew packages.
+- Parent implementation evidence: `kd-1mr.2` implementation commit
+  `440ac7e5e8876b54345682f23dde271889682c40` added the sqlite, bzip2,
+  and xz pilot Formulae, generic Node smoke, non-hello link manifests, and
+  sidecar browser-smoke status fields.
+
+This is a design artifact. It does not implement the browser harness, change
+sidecar schemas, publish Homebrew bottles, or mark any non-hello package as
+browser-compatible.
+
+## Problem Statement
+
+The Homebrew pilot can now build and smoke sqlite, bzip2, and xz through the
+Node Homebrew VFS path, but browser compatibility is still effectively
+hardcoded to the original `hello` smoke. That leaves generated sidecars with
+truthful `browser_smoke` skips for non-hello packages, but no reusable way to
+turn package-specific browser evidence into:
+
+- a browser-compatible sidecar claim for wasm32 packages that actually boot
+  and run in the browser host;
+- a failed or skipped browser-smoke outcome when the browser host, VFS image,
+  package command, or validation consumer does not work;
+- durable passed, failed, and skipped browser outcome-list artifacts with
+  reasons.
+
+The new path must prove browser behavior through the normal browser product
+surface: a precomposed Homebrew-derived VFS image, the Kandelo browser app,
+the browser kernel host, a real terminal or runtime process, and package
+commands that run from the poured Homebrew prefix.
+
+## Non-Goals
+
+- Do not add package-specific browser demo branches for sqlite, bzip2, or xz.
+- Do not mark browser support from Node smoke, bottle build success, or VFS
+  image construction alone.
+- Do not require guest `brew install`; the supported path is still
+  precomposed VFS images built from sidecars and verified bottle bytes.
+- Do not weaken ABI, cache-key, bottle sha, byte-count, link-manifest, or
+  receipt validation to get a browser image to boot.
+- Do not make full SQLite upstream project tests part of this browser smoke.
+  The browser smoke only proves the package-specific consumer case.
+- Do not make wasm64 browser compatibility claims. The existing sidecar
+  wrapper only permits browser success for wasm32, and the gallery path is
+  wasm32-only.
+
+## Users And Operator Workflows
+
+### Package Porter
+
+The porter runs one command against a generated or trusted tap root and gets
+per-formula browser status:
+
+```bash
+npx tsx scripts/homebrew-package-browser-smoke.ts \
+  --tap-root /path/to/kandelo-homebrew \
+  --formula sqlite \
+  --formula bzip2 \
+  --formula xz \
+  --arch wasm32 \
+  --result-dir test-runs/homebrew-package-browser-smoke
+```
+
+The command builds candidate Homebrew VFS images, launches the browser app,
+runs package-specific checks, and writes summary plus outcome-list artifacts.
+
+### Trusted Publisher
+
+The trusted workflow uses the browser-smoke summary to decide whether the
+final sidecars may record:
+
+- `runtime_support = ["node", "browser"]` and
+  `browser_compatible = true`, when the wasm32 browser smoke passes; or
+- `runtime_support = ["node"]`, `browser_compatible = false`, and a failed or
+  skipped `browser_smoke` outcome with artifact paths and reasons.
+
+Browser gallery assets are generated only after final metadata records wasm32
+success and `browser_compatible = true`.
+
+### Maintainer Reviewer
+
+The reviewer checks that the browser outcome is evidence-backed, not inferred.
+They should be able to inspect the command, browser URL, terminal output,
+VFS build report, screenshot or trace, and passed/failed/skipped TSV files for
+each package.
+
+### Debugger
+
+The debugger needs failures to identify the layer: sidecar planning, bottle
+fetch, VFS build, SQLite consumer compilation, Vite startup, browser app boot,
+cross-origin isolation, terminal readiness, package command exit, output
+matching, or sidecar finalization.
+
+## Current State
+
+The parent Homebrew pilot commit contains these relevant surfaces:
+
+- `scripts/homebrew-package-node-smoke.ts` materializes Homebrew sidecars into
+  VFS images and runs package-specific Node smokes. It writes `summary.json`,
+  `summary.md`, `failures.json`, `current-run.json`, and
+  `outcome-lists/{passed,failed,skipped}-tests.tsv`.
+- `images/vfs/scripts/build-homebrew-vfs-image.ts` builds precomposed VFS
+  images from Homebrew sidecars and verified bottle bytes.
+- `apps/browser-demos/test/kandelo-homebrew.spec.ts` has the trusted `hello`
+  browser smoke. It boots the browser app with `?vfs=`, waits for
+  the terminal prompt, types `/home/linuxbrew/.linuxbrew/bin/hello --version`,
+  and checks terminal output.
+- `scripts/homebrew-generate-sidecars-from-env.sh` can record browser smoke
+  as `success`, `failed`, or `skipped` through environment variables. Success
+  requires VFS image, VFS report, browser URL, and browser command evidence.
+- `homebrew/kandelo-homebrew/Kandelo/provenance.schema.json` already supports
+  a `browser_smoke` outcome list with `passed`, `failed`, `skipped`, and
+  `skip_reason` strings. No schema change is required to record package-level
+  browser case names and artifact paths as strings.
+
+The child bead worktree created for this design was based on convoy commit
+`f4339836e8c9c4b1fc2de7f4d931856c51a74432`, which does not contain the parent
+Homebrew pilot files. Implementation for `kd-1mr.2.1` should either stack on
+`440ac7e5e8876b54345682f23dde271889682c40` or wait until that parent work is
+merged before editing the harness.
+
+## Architecture
+
+Add a reusable TypeScript browser smoke runner:
+
+```text
+scripts/homebrew-package-browser-smoke.ts
+```
+
+The runner should mirror the Node smoke runner's contract:
+
+```text
+tap root + formula list + arch
+      |
+      v
+read Kandelo/metadata.json
+      |
+      v
+plan and build candidate Homebrew VFS images
+  - verify ABI, cache key when supplied, bottle sha, bottle bytes
+  - load link manifests
+  - do not require browser_compatible=true before the smoke runs
+      |
+      v
+optionally inject validation-only smoke artifacts
+  - sqlite_basic.wasm for sqlite
+      |
+      v
+save one browser candidate image per formula
+      |
+      v
+serve image through Vite/browser app
+      |
+      v
+run package-specific terminal commands
+      |
+      v
+write browser summary and outcome-list artifacts
+      |
+      v
+final sidecar generation consumes summary
+```
+
+The important detail is the planning mode. Before a package has browser
+evidence, its sidecar must not yet say `browser_compatible = true`. The smoke
+runner therefore must not call `planHomebrewVfs()` with `runtime: "browser"`
+for candidate images. It should omit the runtime filter, or use the existing
+Node-compatible success metadata, then treat the browser run as the evidence
+that may update final sidecars. The stricter `runtime: "browser"` check is
+appropriate after final metadata is generated, especially for gallery asset
+creation.
+
+## Package Smoke Cases
+
+Keep package behavior in a small shared case registry, for example:
+
+```text
+scripts/homebrew-package-smoke-cases.ts
+```
+
+The Node and browser runners do not need identical mechanics, but they should
+share the same package intent: command, expected output, minimum success
+threshold, and unsupported reasons.
+
+### Bzip2
+
+Required browser case:
+
+```bash
+/home/linuxbrew/.linuxbrew/bin/bzip2 --help
+```
+
+Expected output should match `/bzip2/i` in terminal text. `--help` is a good
+first check because the parent Node smoke used it to avoid `bzip2` writing
+compressed bytes to a terminal.
+
+Preferred stronger case:
+
+```bash
+cd /tmp &&
+printf 'kandelo bzip2 browser smoke\n' > bzip2.in &&
+/home/linuxbrew/.linuxbrew/bin/bzip2 -k bzip2.in &&
+/home/linuxbrew/.linuxbrew/bin/bzip2 -dc bzip2.in.bz2
+```
+
+Expected output is the original text. This avoids writing compressed bytes to
+the PTY. If this round trip is flaky because of shell or file semantics, the
+harness should mark that specific case skipped with a reason, not convert the
+package to browser-compatible on a hidden workaround.
+
+### Xz
+
+Required browser case:
+
+```bash
+/home/linuxbrew/.linuxbrew/bin/xz --version
+```
+
+Expected output should match `/xz/i`.
+
+Preferred stronger case:
+
+```bash
+cd /tmp &&
+printf 'kandelo xz browser smoke\n' > xz.in &&
+/home/linuxbrew/.linuxbrew/bin/xz -k xz.in &&
+/home/linuxbrew/.linuxbrew/bin/xz -dc xz.in.xz
+```
+
+Expected output is the original text. As with bzip2, round-trip inability is a
+case result, not a reason to fake success.
+
+### SQLite
+
+SQLite is a library package, so the browser smoke must run a test-only
+consumer. Reuse the parent Node smoke's idea:
+
+1. Extract `include/sqlite3.h`, `include/sqlite3ext.h`, and
+   `lib/libsqlite3.a` from the poured keg in the candidate VFS.
+2. Compile `packages/registry/sqlite/test/sqlite_basic.c` with the
+   worktree-local SDK into `sqlite_basic.wasm`.
+3. Inject the validation-only Wasm into the candidate browser VFS at a path
+   such as `/usr/local/kandelo-smoke/bin/sqlite_basic`.
+4. Boot the image in the browser and run:
+
+```bash
+/usr/local/kandelo-smoke/bin/sqlite_basic
+```
+
+Browser compatibility for sqlite requires the consumer to exit 0 and print
+the existing `PASS` marker. A library bottle that builds, pours, or links is
+not sufficient by itself.
+
+If consumer compilation is unavailable in a particular environment, record
+`browser_smoke` as skipped with the compile blocker and artifact path. Do not
+mark sqlite as browser-compatible.
+
+## Harness Behavior
+
+The browser runner should:
+
+- accept `--tap-root`, repeated `--formula`, `--arch`, `--result-dir`,
+  `--bottle-cache`, `--timeout-ms`, `--max-bytes`, and `--bead-id`;
+- reject or skip `--arch wasm64` for browser compatibility with a clear
+  reason;
+- create one result subdirectory per formula and arch;
+- build a candidate VFS image with Homebrew VFS metadata preserved in image
+  metadata;
+- use a deterministic static fixture path under
+  `apps/browser-demos/public/__kandelo-homebrew-smoke//` or a local
+  static server so `?vfs=` can fetch the image;
+- start Vite with `KANDELO_BROWSER_TEST_NO_HMR=1`;
+- launch Chromium with SharedArrayBuffer support, matching the existing
+  browser smoke baseline;
+- navigate to `/?vfs=`;
+- wait for the terminal prompt rather than sleeping blindly;
+- type a package command wrapped with a unique pass/fail sentinel;
+- collect terminal text, console warnings/errors, page errors, request
+  failures, screenshots, and Playwright trace or video when available;
+- write `current-run.json` during execution so interrupted runs are
+  diagnosable;
+- always write passed, failed, and skipped outcome lists, even when one list is
+  empty.
+
+The existing app-level `LiveKernelHost.runShellCommand()` and
+`/etc/kandelo/demo.json` `autoCommand` path are useful references, but the
+first implementation should keep command execution in the harness. That avoids
+creating product-visible auto-run metadata solely for tests. If terminal
+driving proves too flaky, a later refinement can add smoke-only demo metadata
+to the generated VFS image and still boot through the normal app path.
+
+## Sidecar Finalization
+
+Avoid a circular metadata dependency by splitting candidate sidecars from
+final sidecars:
+
+1. Generate candidate sidecars after bottle build with browser smoke skipped
+   or absent. These sidecars are enough to materialize a candidate VFS image.
+2. Run Node and browser smokes against the candidate sidecars.
+3. Regenerate final sidecars with Node and browser summary inputs.
+4. Publish final sidecars, provenance, and release assets.
+
+Extend `scripts/homebrew-generate-sidecars-from-env.sh` to accept an optional
+browser summary path:
+
+```text
+KANDELO_HOMEBREW_BROWSER_SMOKE_SUMMARY=/path/to/summary.json
+```
+
+When present, the wrapper should populate the provenance `browser_smoke`
+outcome from the summary:
+
+- `status = success` only when all required package browser cases pass;
+- `status = failed` when any required case fails;
+- `status = skipped` when the package is unsupported for browser smoke or a
+  prerequisite is missing;
+- `passed`, `failed`, and `skipped` arrays contain concrete case names,
+  reasons, and artifact paths as strings.
+
+The existing environment variables remain valid for manual or transitional
+use:
+
+```text
+KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS
+KANDELO_HOMEBREW_BROWSER_SMOKE_REASON
+KANDELO_HOMEBREW_VFS_IMAGE
+KANDELO_HOMEBREW_VFS_REPORT
+KANDELO_HOMEBREW_BROWSER_SMOKE_URL
+KANDELO_HOMEBREW_BROWSER_SMOKE_COMMAND
+KANDELO_HOMEBREW_GALLERY_ROOT
+```
+
+Final metadata rules:
+
+- `browser_compatible = true` only for wasm32 packages with successful
+  browser smoke.
+- `runtime_support` includes `browser` only when `browser_compatible = true`.
+- Browser failure does not have to make the bottle status `failed` if the
+  package is still valid for Node. It should remain a successful Node-capable
+  bottle with a failed browser-smoke validation outcome.
+- Gallery creation runs only after final metadata passes
+  `homebrew-validate` and records browser compatibility.
+
+## Outcome Artifacts
+
+For each browser smoke run, write:
+
+- `summary.json` - machine-readable suite result, counts, tap commit,
+  image/report paths, formulas, arch, browser URL, and command list.
+- `summary.md` - reviewer-readable table of cases and statuses.
+- `current-run.json` - live progress, current case, expected next action, and
+  stale-run threshold.
+- `failures.json` - complete failed case objects.
+- `outcome-lists/passed-tests.tsv` - case, duration, details, artifact path.
+- `outcome-lists/failed-tests.tsv` - case, duration, error, artifact path.
+- `outcome-lists/skipped-tests.tsv` - case, reason, artifact path.
+- `--homebrew.vfs.zst` - candidate image or a pointer to its
+  served copy.
+- `--homebrew-vfs-report.json` - VFS builder report.
+- screenshots and traces when the browser page fails, times out, or produces
+  unexpected output.
+
+Skipped lists must include reasons, for example:
+
+- `wasm64 browser compatibility is unsupported by the current Homebrew browser
+  sidecar path`;
+- `sqlite consumer compilation failed before browser launch`;
+- `candidate sidecar planning failed because package has no wasm32 bottle`;
+- `browser terminal did not become ready before timeout`.
+
+## Implementation Sequence
+
+1. Stack the worktree on the parent Homebrew pilot commit
+   `440ac7e5e8876b54345682f23dde271889682c40` or wait until it is merged.
+2. Refactor shared outcome-list helpers from
+   `scripts/homebrew-package-node-smoke.ts` into a small local helper module.
+3. Add shared package smoke case definitions for sqlite, bzip2, and xz.
+4. Add `scripts/homebrew-package-browser-smoke.ts` with candidate VFS
+   building, optional sqlite consumer injection, Vite startup, Playwright
+   browser execution, terminal command driving, and artifact writing.
+5. Add targeted unit coverage for command planning, unsupported-case
+   classification, and outcome-list generation.
+6. Extend `scripts/homebrew-generate-sidecars-from-env.sh` to consume browser
+   summary JSON and populate the existing `browser_smoke` provenance outcome.
+7. Update the reusable Homebrew publish workflow to run the browser smoke only
+   for wasm32 entries after candidate sidecars are available and before final
+   sidecars are published.
+8. Keep `apps/browser-demos/test/kandelo-homebrew.spec.ts` for gallery gating
+   and hello coverage. Add only minimal coverage there if the generic runner
+   exposes a browser-app regression not covered by the direct script.
+9. Generate browser gallery assets only from final metadata that passed the
+   browser smoke.
+10. Update `docs/homebrew-publishing.md` with the generic browser smoke flow,
+   candidate/final sidecar sequence, outcome artifacts, and unsupported wasm64
+   boundary.
+
+## Test And Documentation Plan
+
+For the implementation PR, run and record:
+
+- `npx tsx scripts/homebrew-package-node-smoke.ts --tap-root  --formula sqlite --formula bzip2 --formula xz --arch wasm32 --result-dir `
+- `npx tsx scripts/homebrew-package-browser-smoke.ts --tap-root  --formula sqlite --formula bzip2 --formula xz --arch wasm32 --result-dir `
+- `cargo run --release -p xtask -- homebrew-validate --tap-root `
+- `cd host && npx vitest run test/homebrew-vfs-planner.test.ts test/homebrew-vfs-builder.test.ts test/homebrew-vfs-fetch.test.ts`
+- `cd apps/browser-demos && npx playwright test test/kandelo-homebrew.spec.ts --project=chromium`
+- `scripts/validate-software-gallery.mjs ` when gallery assets are generated.
+
+If implementation changes shared host runtime, VFS semantics, ABI-adjacent
+code, package bytes, or browser app boot behavior, broaden verification using
+the suites in `CLAUDE.md` and `docs/agent-guidance/validation.md`. At minimum,
+publish exactly which full-gate commands were run and which were not run.
+
+Documentation updates:
+
+- `docs/homebrew-publishing.md` for the generic browser smoke workflow and
+  sidecar finalization semantics.
+- Package-specific notes only if the implementation changes package build
+  outputs or smoke expectations.
+- No `docs/package-management.md` update unless package archive, resolver, or
+  revision semantics change.
+
+## Alternatives Considered
+
+### Add One Playwright Spec Per Package
+
+Rejected. Per-package specs would reproduce the current `hello` hardcoding and
+make every new Formula require browser test plumbing. A generic runner keeps
+package specifics in data and produces uniform artifacts.
+
+### Run Browser Smokes Through The Low-Level Test Runner Page
+
+Rejected as the default. `apps/browser-demos/pages/test-runner` is useful for
+isolated Wasm binaries, but Homebrew browser support is a product claim about
+precomposed VFS images, the Kandelo app boot path, browser host setup, and
+terminal-visible execution.
+
+### Require `browser_compatible=true` Before Building The Smoke Image
+
+Rejected because it is circular. The smoke must build a candidate image before
+browser compatibility is known. The strict browser runtime filter belongs
+after final sidecar generation.
+
+### Mark Browser Compatibility From Node Smoke
+
+Rejected. Node and browser are peer hosts, and browser-specific failures in
+fetching, SharedArrayBuffer setup, VFS restore, terminal PTY behavior, or
+worker lifecycle would be hidden.
+
+### Put `autoCommand` In Every Smoke Image
+
+Deferred. Image-declared `autoCommand` is a real product feature and should
+not be used only to simplify the first test harness. It remains a fallback if
+terminal driving is demonstrably flaky.
+
+## Risks And Mitigations
+
+Metadata circularity:
+
+- Risk: the harness needs browser-compatible metadata to build the image that
+  proves browser compatibility.
+- Mitigation: use candidate sidecars without a browser runtime filter, then
+  regenerate final sidecars from smoke summaries.
+
+Terminal flakiness:
+
+- Risk: prompt detection or text wrapping makes browser checks unreliable.
+- Mitigation: use unique sentinels, bounded waits, screenshots/traces on
+  failure, and one command at a time. Avoid relying on exact full terminal
+  layout.
+
+Library smoke ambiguity:
+
+- Risk: sqlite could be marked compatible after only headers and libraries are
+  linked into the VFS.
+- Mitigation: require a compiled `sqlite_basic.wasm` consumer to run in the
+  browser before setting browser compatibility.
+
+Package command overreach:
+
+- Risk: bzip2/xz round trips could fail because shell utilities are missing
+  rather than because the package is broken.
+- Mitigation: make version/help the required first smoke, use Bash builtins for
+  file creation, and record round-trip skips separately if needed.
+
+Workflow time and artifact size:
+
+- Risk: one browser boot per formula increases trusted publication time.
+- Mitigation: run only wasm32 browser candidates, keep timeouts explicit, cache
+  bottle bytes, and preserve per-formula images only as run artifacts unless a
+  package becomes gallery-eligible.
+
+Stale branch base:
+
+- Risk: implementation starts from a branch that lacks the parent Homebrew
+  pilot files.
+- Mitigation: stack on the parent implementation commit or merged main before
+  editing. Record the base in bead metadata.
+
+## Open Questions
+
+- Should browser compatibility require the bzip2/xz file round-trip cases, or
+  is version/help sufficient for the first generic harness?
+- Should the browser runner use Playwright Test for trace integration, or a
+  direct Playwright script like `browser-sqlite-official-runner.ts` for simpler
+  process control?
+- Should sqlite consumer injection be local to the smoke runner, or should
+  `build-homebrew-vfs-image.ts` grow a generic `--inject-file` test-only
+  option?
+- Should the trusted workflow run browser smoke before or after Node smoke
+  finalization, or should both consume the same candidate sidecar root and
+  regenerate final sidecars once?
+- Which browser projects beyond Chromium should become required after the
+  generic harness is stable?
diff --git a/homebrew/kandelo-homebrew/Formula/bzip2.rb b/homebrew/kandelo-homebrew/Formula/bzip2.rb
new file mode 100644
index 0000000000..257fee342e
--- /dev/null
+++ b/homebrew/kandelo-homebrew/Formula/bzip2.rb
@@ -0,0 +1,68 @@
+require "shellwords"
+
+class Bzip2 < Formula
+  desc "bzip2 compression tool for Kandelo"
+  homepage "https://sourceware.org/bzip2/"
+  url "https://sourceware.org/pub/bzip2/bzip2-1.0.8.tar.gz"
+  sha256 "ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269"
+  license "bzip2-1.0.6"
+
+  skip_clean "bin/bzip2"
+
+  def kandelo_root
+    root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"]
+    odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty?
+    root
+  end
+
+  def configure_kandelo_environment(root)
+    %w[
+      CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH
+      C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT
+      MACOSX_DEPLOYMENT_TARGET
+    ].each { |key| ENV.delete(key) }
+
+    ENV.prepend_path "PATH", "#{root}/sdk/bin"
+    if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != ""
+      ENV.prepend_path "PATH", File.dirname(node)
+    end
+    if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != ""
+      ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin
+      ENV["LLVM_BIN"] = llvm_bin
+      ENV.prepend_path "PATH", llvm_bin
+    end
+  end
+
+  def install
+    root = kandelo_root
+    configure_kandelo_environment(root)
+
+    out_dir = buildpath/"kandelo-package-out"
+    ENV["WASM_POSIX_DEP_VERSION"] = version.to_s
+    ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://sourceware.org/pub/bzip2/bzip2-#{version}.tar.gz"
+    ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269"
+    ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir
+    ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work"
+    ENV["WASM_POSIX_DEP_TARGET_ARCH"] = ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32"))
+
+    system "bash", "#{root}/packages/registry/bzip2/build-bzip2.sh"
+    chmod 0755, out_dir/"bzip2.wasm"
+    bin.install out_dir/"bzip2.wasm" => "bzip2"
+    chmod 0755, bin/"bzip2"
+  end
+
+  test do
+    bzip2 = bin/"bzip2"
+    assert_equal "\0asm".b, File.binread(bzip2, 4)
+
+    root = kandelo_root
+    configure_kandelo_environment(root)
+
+    test_wasm = testpath/"bzip2.wasm"
+    File.binwrite(test_wasm, File.binread(bzip2))
+    output = shell_output(
+      "cd #{root.shellescape} && node --experimental-wasm-exnref --import tsx/esm examples/run-example.ts #{test_wasm.to_s.shellescape} --help 2>&1",
+    )
+    assert_match "bzip2", output.scrub
+  end
+end
diff --git a/homebrew/kandelo-homebrew/Formula/sqlite.rb b/homebrew/kandelo-homebrew/Formula/sqlite.rb
new file mode 100644
index 0000000000..914b207a6c
--- /dev/null
+++ b/homebrew/kandelo-homebrew/Formula/sqlite.rb
@@ -0,0 +1,89 @@
+require "shellwords"
+
+class Sqlite < Formula
+  desc "SQLite static library for Kandelo"
+  homepage "https://www.sqlite.org/"
+  url "https://www.sqlite.org/2025/sqlite-amalgamation-3490100.zip"
+  sha256 "6cebd1d8403fc58c30e93939b246f3e6e58d0765a5cd50546f16c00fd805d2c3"
+  license "blessing"
+
+  def kandelo_root
+    root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"]
+    odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty?
+    root
+  end
+
+  def kandelo_arch
+    ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32"))
+  end
+
+  def kandelo_tool_prefix
+    case kandelo_arch
+    when "wasm32" then "wasm32posix"
+    when "wasm64" then "wasm64posix"
+    else odie "unsupported HOMEBREW_KANDELO_ARCH=#{kandelo_arch}"
+    end
+  end
+
+  def configure_kandelo_environment(root)
+    %w[
+      CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH
+      C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT
+      MACOSX_DEPLOYMENT_TARGET
+    ].each { |key| ENV.delete(key) }
+
+    ENV.prepend_path "PATH", "#{root}/sdk/bin"
+    if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != ""
+      ENV.prepend_path "PATH", File.dirname(node)
+    end
+    if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != ""
+      ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin
+      ENV["LLVM_BIN"] = llvm_bin
+      ENV.prepend_path "PATH", llvm_bin
+    end
+  end
+
+  def install
+    root = kandelo_root
+    configure_kandelo_environment(root)
+
+    out_dir = buildpath/"kandelo-package-out"
+    ENV["WASM_POSIX_DEP_VERSION"] = version.to_s
+    ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://www.sqlite.org/2025/sqlite-amalgamation-3490100.zip"
+    ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "6cebd1d8403fc58c30e93939b246f3e6e58d0765a5cd50546f16c00fd805d2c3"
+    ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir
+    ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work"
+    ENV["WASM_POSIX_DEP_TARGET_ARCH"] = kandelo_arch
+
+    system "bash", "#{root}/packages/registry/sqlite/build-sqlite.sh"
+    include.install out_dir/"include/sqlite3.h"
+    include.install out_dir/"include/sqlite3ext.h"
+    lib.install out_dir/"lib/libsqlite3.a"
+    (lib/"pkgconfig").install out_dir/"lib/pkgconfig/sqlite3.pc"
+    inreplace lib/"pkgconfig/sqlite3.pc", /^prefix=.*/, "prefix=#{prefix}"
+  end
+
+  test do
+    root = kandelo_root
+    configure_kandelo_environment(root)
+
+    test_src = testpath/"sqlite_basic.c"
+    test_wasm = testpath/"sqlite_basic.wasm"
+    FileUtils.cp "#{root}/packages/registry/sqlite/test/sqlite_basic.c", test_src
+
+    system "#{kandelo_tool_prefix}-cc",
+      "-I#{include}",
+      test_src,
+      "#{lib}/libsqlite3.a",
+      "-lm",
+      "-o",
+      test_wasm
+    assert_equal "\0asm".b, File.binread(test_wasm, 4)
+
+    output = shell_output(
+      "cd #{root.shellescape} && node --experimental-wasm-exnref --import tsx/esm examples/run-example.ts #{test_wasm.to_s.shellescape}",
+    )
+    assert_match "PASS", output
+    assert_match "prefix=#{prefix}", File.read(lib/"pkgconfig/sqlite3.pc")
+  end
+end
diff --git a/homebrew/kandelo-homebrew/Formula/xz.rb b/homebrew/kandelo-homebrew/Formula/xz.rb
new file mode 100644
index 0000000000..e9f1da378e
--- /dev/null
+++ b/homebrew/kandelo-homebrew/Formula/xz.rb
@@ -0,0 +1,68 @@
+require "shellwords"
+
+class Xz < Formula
+  desc "XZ Utils compression tool for Kandelo"
+  homepage "https://tukaani.org/xz/"
+  url "https://tukaani.org/xz/xz-5.6.2.tar.xz"
+  sha256 "a9db3bb3d64e248a0fae963f8fb6ba851a26ba1822e504dc0efd18a80c626caf"
+  license all_of: ["GPL-2.0-or-later", "LGPL-2.1-or-later", "0BSD"]
+
+  skip_clean "bin/xz"
+
+  def kandelo_root
+    root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"]
+    odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty?
+    root
+  end
+
+  def configure_kandelo_environment(root)
+    %w[
+      CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH
+      C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT
+      MACOSX_DEPLOYMENT_TARGET
+    ].each { |key| ENV.delete(key) }
+
+    ENV.prepend_path "PATH", "#{root}/sdk/bin"
+    if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != ""
+      ENV.prepend_path "PATH", File.dirname(node)
+    end
+    if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != ""
+      ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin
+      ENV["LLVM_BIN"] = llvm_bin
+      ENV.prepend_path "PATH", llvm_bin
+    end
+  end
+
+  def install
+    root = kandelo_root
+    configure_kandelo_environment(root)
+
+    out_dir = buildpath/"kandelo-package-out"
+    ENV["WASM_POSIX_DEP_VERSION"] = version.to_s
+    ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://tukaani.org/xz/xz-#{version}.tar.xz"
+    ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "a9db3bb3d64e248a0fae963f8fb6ba851a26ba1822e504dc0efd18a80c626caf"
+    ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir
+    ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work"
+    ENV["WASM_POSIX_DEP_TARGET_ARCH"] = ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32"))
+
+    system "bash", "#{root}/packages/registry/xz/build-xz.sh"
+    chmod 0755, out_dir/"xz.wasm"
+    bin.install out_dir/"xz.wasm" => "xz"
+    chmod 0755, bin/"xz"
+  end
+
+  test do
+    xz = bin/"xz"
+    assert_equal "\0asm".b, File.binread(xz, 4)
+
+    root = kandelo_root
+    configure_kandelo_environment(root)
+
+    test_wasm = testpath/"xz.wasm"
+    File.binwrite(test_wasm, File.binread(xz))
+    output = shell_output(
+      "cd #{root.shellescape} && node --experimental-wasm-exnref --import tsx/esm examples/run-example.ts #{test_wasm.to_s.shellescape} --version",
+    )
+    assert_match "xz", output.scrub
+  end
+end
diff --git a/host/src/browser-kernel-host.ts b/host/src/browser-kernel-host.ts
index 58fe444ddc..7990797a1a 100644
--- a/host/src/browser-kernel-host.ts
+++ b/host/src/browser-kernel-host.ts
@@ -23,7 +23,6 @@ import type { HttpRequest, HttpResponse } from "./networking/in-kernel-http";
 
 export type { HttpRequest, HttpResponse };
 import kernelWasmUrl from "@kernel-wasm?url";
-import rootfsVfsUrl from "@rootfs-vfs?url";
 import workerEntryUrl from "./worker-entry-browser.ts?worker&url";
 import kernelWorkerEntryUrl from "./browser-kernel-worker-entry.ts?worker&url";
 import { DEFAULT_MAX_PAGES } from "./constants";
@@ -254,13 +253,13 @@ export class BrowserKernel {
       kernelWasmBytes
         ? Promise.resolve(kernelWasmBytes)
         : fetch(kernelWasmUrl).then((r) => r.arrayBuffer()),
-      fetch(rootfsVfsUrl).then((r) => r.arrayBuffer()),
+      loadDefaultRootfsImage(),
     ]);
 
     await this.bootWorker({
       kernelWasmBytes: wasmBytes,
       fsSab: this.fsSab!,
-      rootfsImage: new Uint8Array(rootfsVfsBuf),
+      rootfsImage: rootfsVfsBuf,
     });
 
     await registerLazyVfsMetadata(this.memfs!, async (message) => {
@@ -294,9 +293,7 @@ export class BrowserKernel {
         ? Promise.resolve(options.kernelWasm)
         : fetch(kernelWasmUrl).then((r) => r.arrayBuffer()),
       options.vfsImage === "default"
-        ? fetch(rootfsVfsUrl)
-            .then((r) => r.arrayBuffer())
-            .then((b) => new Uint8Array(b))
+        ? loadDefaultRootfsImage()
         : Promise.resolve(options.vfsImage),
     ]);
 
@@ -1127,3 +1124,9 @@ export class BrowserKernel {
     return this.fbMemoryByPid.get(pid);
   }
 }
+
+async function loadDefaultRootfsImage(): Promise {
+  const { default: rootfsVfsUrl } = await import("./default-rootfs-url");
+  const bytes = await fetch(rootfsVfsUrl).then((r) => r.arrayBuffer());
+  return new Uint8Array(bytes);
+}
diff --git a/host/src/default-rootfs-url.ts b/host/src/default-rootfs-url.ts
new file mode 100644
index 0000000000..5a73ad3895
--- /dev/null
+++ b/host/src/default-rootfs-url.ts
@@ -0,0 +1,3 @@
+import rootfsVfsUrl from "@rootfs-vfs?url";
+
+export default rootfsVfsUrl;
diff --git a/host/test/browser-kernel.test.ts b/host/test/browser-kernel.test.ts
index 7107bb115a..3c47ae5565 100644
--- a/host/test/browser-kernel.test.ts
+++ b/host/test/browser-kernel.test.ts
@@ -89,6 +89,24 @@ async function makeRootfsImageBuffer(): Promise {
   ) as ArrayBuffer;
 }
 
+async function waitForWorker(index = 0): Promise {
+  for (let i = 0; i < 50; i++) {
+    const worker = MockWorker.instances[index];
+    if (worker) return worker;
+    await new Promise((r) => setTimeout(r, 0));
+  }
+  throw new Error(`worker ${index} was not created`);
+}
+
+async function waitForMessage(worker: MockWorker, type: string): Promise {
+  for (let i = 0; i < 50; i++) {
+    const message = worker.lastMessage(type);
+    if (message) return message;
+    await new Promise((r) => setTimeout(r, 0));
+  }
+  throw new Error(`worker message ${type} was not sent`);
+}
+
 describe("BrowserKernel", () => {
   beforeEach(() => {
     MockWorker.instances = [];
@@ -170,14 +188,12 @@ describe("BrowserKernel", () => {
     let resolved = false;
     void initPromise.then(() => { resolved = true; });
 
-    await new Promise((r) => setTimeout(r, 0));
-    const w = MockWorker.instances[0]!;
-    expect(w.lastMessage("init")).toBeDefined();
+    const w = await waitForWorker();
+    await waitForMessage(w, "init");
     w.simulateMessage({ type: "ready" });
     await new Promise((r) => setTimeout(r, 0));
 
-    const lazy = w.lastMessage("register_lazy_files");
-    expect(lazy).toBeDefined();
+    const lazy = await waitForMessage(w, "register_lazy_files");
     expect(typeof lazy.requestId).toBe("number");
     expect(lazy.entries).toMatchObject([
       { path: "/bin/lazy", url: "/assets/lazy.wasm", size: 123 },
@@ -202,12 +218,11 @@ describe("BrowserKernel", () => {
     const kernel = new BrowserKernel({ memfs });
     const initPromise = kernel.init(new ArrayBuffer(8));
 
-    await new Promise((r) => setTimeout(r, 0));
-    const w = MockWorker.instances[0]!;
+    const w = await waitForWorker();
     w.simulateMessage({ type: "ready" });
     await new Promise((r) => setTimeout(r, 0));
 
-    const lazy = w.lastMessage("register_lazy_files");
+    const lazy = await waitForMessage(w, "register_lazy_files");
     w.simulateMessage({
       type: "response",
       requestId: lazy.requestId,
diff --git a/packages/registry/bzip2/build-bzip2.sh b/packages/registry/bzip2/build-bzip2.sh
index 4248060501..8ee4c9417c 100755
--- a/packages/registry/bzip2/build-bzip2.sh
+++ b/packages/registry/bzip2/build-bzip2.sh
@@ -4,42 +4,70 @@ set -euo pipefail
 # Build bzip2 1.0.8 for wasm32-posix-kernel.
 #
 # Plain Makefile build with CC/AR/RANLIB overrides.
-# Output: packages/registry/bzip2/bin/bzip2.wasm
-# Also installs libbz2.a + bzlib.h to sysroot.
+# Output: bzip2.wasm. Resolver/Homebrew invocations install only the
+# declared program output into WASM_POSIX_DEP_OUT_DIR; direct legacy
+# invocations still populate packages/registry/bzip2/bin and sysroot.
 
-BZIP2_VERSION="${BZIP2_VERSION:-1.0.8}"
 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
 REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
-SRC_DIR="$SCRIPT_DIR/bzip2-src"
-BIN_DIR="$SCRIPT_DIR/bin"
-SYSROOT="$REPO_ROOT/sysroot"
+source "$REPO_ROOT/sdk/activate.sh"
+
+BZIP2_VERSION="${WASM_POSIX_DEP_VERSION:-${BZIP2_VERSION:-1.0.8}}"
+SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://sourceware.org/pub/bzip2/bzip2-${BZIP2_VERSION}.tar.gz}"
+SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-}"
+WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}"
+SRC_DIR="$WORK_DIR/bzip2-src"
+BIN_DIR="$WORK_DIR/bin"
+INSTALL_DIR="${WASM_POSIX_DEP_OUT_DIR:-}"
+TARGET_ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}"
+
+if [ "$TARGET_ARCH" != "wasm32" ]; then
+    echo "ERROR: bzip2 is currently packaged for wasm32 only, got $TARGET_ARCH" >&2
+    exit 2
+fi
+
+SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}"
+export WASM_POSIX_SYSROOT="$SYSROOT"
 
 # --- Prerequisites ---
 if ! command -v wasm32posix-cc &>/dev/null; then
-    echo "ERROR: wasm32posix-cc not found. Run 'npm link' in sdk/ first." >&2
+    echo "ERROR: wasm32posix-cc not found. Run through scripts/dev-shell.sh." >&2
     exit 1
 fi
 
 if [ ! -f "$SYSROOT/lib/libc.a" ]; then
-    echo "ERROR: sysroot not found. Run: bash build.sh && bash scripts/build-musl.sh" >&2
+    echo "ERROR: sysroot not found at $SYSROOT. Run scripts/build-musl.sh first." >&2
     exit 1
 fi
 
-export WASM_POSIX_SYSROOT="$SYSROOT"
-
 # --- Download bzip2 source ---
+expected_marker="$(printf '%s\n%s\n%s\n' "$BZIP2_VERSION" "$SOURCE_URL" "$SOURCE_SHA256")"
+SOURCE_MARKER="$SRC_DIR/.kandelo-bzip2-source"
+if [ -d "$SRC_DIR" ] && [ "$(cat "$SOURCE_MARKER" 2>/dev/null || true)" != "$expected_marker" ]; then
+    echo "==> Existing bzip2 source does not match requested version/source; cleaning..."
+    rm -rf "$SRC_DIR"
+fi
+
 if [ ! -d "$SRC_DIR" ]; then
     echo "==> Downloading bzip2 $BZIP2_VERSION..."
-    TARBALL="bzip2-${BZIP2_VERSION}.tar.gz"
-    URL="https://sourceware.org/pub/bzip2/${TARBALL}"
-    curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$URL" -o "/tmp/$TARBALL"
+    tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/kandelo-bzip2-src.XXXXXX")"
+    trap 'rm -rf "$tmpdir"' EXIT
+    TARBALL="$tmpdir/bzip2-${BZIP2_VERSION}.tar.gz"
+    curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$SOURCE_URL" -o "$TARBALL"
+    if [ -n "$SOURCE_SHA256" ]; then
+        echo "==> Verifying source sha256..."
+        echo "$SOURCE_SHA256  $TARBALL" | shasum -a 256 -c -
+    fi
     mkdir -p "$SRC_DIR"
-    tar xzf "/tmp/$TARBALL" -C "$SRC_DIR" --strip-components=1
-    rm "/tmp/$TARBALL"
+    tar xzf "$TARBALL" -C "$SRC_DIR" --strip-components=1
+    printf '%s\n' "$expected_marker" > "$SOURCE_MARKER"
+    trap - EXIT
+    rm -rf "$tmpdir"
     echo "==> Source extracted to $SRC_DIR"
 fi
 
 cd "$SRC_DIR"
+make clean >/dev/null 2>&1 || true
 
 # --- Build ---
 echo "==> Building bzip2..."
@@ -64,17 +92,25 @@ else
     exit 1
 fi
 
-# --- Install library to sysroot ---
-echo "==> Installing libbz2.a and bzlib.h to sysroot..."
-cp "$SRC_DIR/libbz2.a" "$SYSROOT/lib/"
-cp "$SRC_DIR/bzlib.h" "$SYSROOT/include/"
-echo "==> Installed libbz2.a and bzlib.h"
+if [ -n "$INSTALL_DIR" ]; then
+    source "$REPO_ROOT/scripts/wasm-artifact-guards.sh"
+    wasm_require_no_legacy_asyncify "$BIN_DIR/bzip2.wasm"
+    wasm_require_no_fork_instrumentation "$BIN_DIR/bzip2.wasm"
+    rm -rf "$INSTALL_DIR"
+    mkdir -p "$INSTALL_DIR"
+    cp "$BIN_DIR/bzip2.wasm" "$INSTALL_DIR/bzip2.wasm"
+    echo "==> Installed bzip2.wasm to $INSTALL_DIR"
+else
+    # --- Install library to sysroot (legacy direct invocation only) ---
+    echo "==> Installing libbz2.a and bzlib.h to sysroot..."
+    cp "$SRC_DIR/libbz2.a" "$SYSROOT/lib/"
+    cp "$SRC_DIR/bzlib.h" "$SYSROOT/include/"
+    echo "==> Installed libbz2.a and bzlib.h"
+
+    source "$REPO_ROOT/scripts/install-local-binary.sh"
+    install_local_binary bzip2 "$SCRIPT_DIR/bin/bzip2.wasm"
+fi
 
 echo ""
 echo "==> bzip2 built successfully!"
 echo "Binary: $BIN_DIR/bzip2.wasm"
-
-# Install into local-binaries/ so the resolver picks the freshly-built
-# binary over the fetched release.
-source "$REPO_ROOT/scripts/install-local-binary.sh"
-install_local_binary bzip2 "$SCRIPT_DIR/bin/bzip2.wasm"
diff --git a/packages/registry/sqlite/build-sqlite.sh b/packages/registry/sqlite/build-sqlite.sh
index 8503250dbc..e7d205b6c1 100755
--- a/packages/registry/sqlite/build-sqlite.sh
+++ b/packages/registry/sqlite/build-sqlite.sh
@@ -14,30 +14,63 @@ set -euo pipefail
 
 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
 REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
-SRC_DIR="$SCRIPT_DIR/sqlite-src"
+source "$REPO_ROOT/sdk/activate.sh"
 
 # --- Resolver contract (with legacy fallbacks) ---
 SQLITE_VERSION="${WASM_POSIX_DEP_VERSION:-${SQLITE_VERSION:-3.49.1}}"
+WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}"
+SRC_DIR="$WORK_DIR/sqlite-src"
 INSTALL_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR/sqlite-install}"
+TARGET_ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}"
 # Legacy default URL uses the packed version form (3.49.1 → 3490100).
 SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://www.sqlite.org/2025/sqlite-amalgamation-3490100.zip}"
 SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-}"
 
+case "$TARGET_ARCH" in
+    wasm32)
+        TOOL_PREFIX="wasm32posix"
+        SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}"
+        ;;
+    wasm64)
+        TOOL_PREFIX="wasm64posix"
+        SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot64}"
+        ;;
+    *)
+        echo "ERROR: unsupported WASM_POSIX_DEP_TARGET_ARCH=$TARGET_ARCH" >&2
+        exit 2
+        ;;
+esac
+export WASM_POSIX_SYSROOT="$SYSROOT"
+
 # CLI is a consumer artifact, not a library. Skip it when invoked via
 # the resolver — it would waste cache space and the consumer-side
 # tooling will build it independently.
 BUILD_CLI=1
 [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ] && BUILD_CLI=0
 
-if ! command -v wasm32posix-cc &>/dev/null; then
-    echo "ERROR: wasm32posix-cc not found. Run 'npm link' in sdk/ first." >&2
+if ! command -v "${TOOL_PREFIX}-cc" &>/dev/null; then
+    echo "ERROR: ${TOOL_PREFIX}-cc not found. Run through scripts/dev-shell.sh." >&2
+    exit 1
+fi
+
+if [ ! -f "$SYSROOT/lib/libc.a" ]; then
+    echo "ERROR: sysroot not found at $SYSROOT. Run scripts/build-musl.sh for $TARGET_ARCH first." >&2
     exit 1
 fi
 
 # --- Fetch + verify source ---
-if [ ! -d "$SRC_DIR/sqlite3.c" ] && [ ! -f "$SRC_DIR/sqlite3.c" ]; then
+expected_marker="$(printf '%s\n%s\n%s\n' "$SQLITE_VERSION" "$SOURCE_URL" "$SOURCE_SHA256")"
+SOURCE_MARKER="$SRC_DIR/.kandelo-sqlite-source"
+if [ -d "$SRC_DIR" ] && [ "$(cat "$SOURCE_MARKER" 2>/dev/null || true)" != "$expected_marker" ]; then
+    echo "==> Existing SQLite source does not match requested version/source; cleaning..."
+    rm -rf "$SRC_DIR"
+fi
+
+if [ ! -f "$SRC_DIR/sqlite3.c" ]; then
     echo "==> Downloading SQLite $SQLITE_VERSION..."
-    TARBALL="/tmp/sqlite-amalgamation.zip"
+    tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/kandelo-sqlite-src.XXXXXX")"
+    trap 'rm -rf "$tmpdir"' EXIT
+    TARBALL="$tmpdir/sqlite-amalgamation.zip"
     curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$SOURCE_URL" -o "$TARBALL"
     if [ -n "$SOURCE_SHA256" ]; then
         echo "==> Verifying source sha256..."
@@ -51,7 +84,9 @@ if [ ! -d "$SRC_DIR/sqlite3.c" ] && [ ! -f "$SRC_DIR/sqlite3.c" ]; then
         mv "$inner"/* "$SRC_DIR/"
         rmdir "$inner"
     fi
-    rm "$TARBALL"
+    printf '%s\n' "$expected_marker" > "$SOURCE_MARKER"
+    trap - EXIT
+    rm -rf "$tmpdir"
 fi
 
 SQLITE_CFLAGS="-O2 \
@@ -68,10 +103,10 @@ SQLITE_CFLAGS="-O2 \
 # --- Compile library ---
 echo "==> Compiling SQLite for Wasm..."
 # shellcheck disable=SC2086
-wasm32posix-cc -c $SQLITE_CFLAGS \
+"${TOOL_PREFIX}-cc" -c $SQLITE_CFLAGS \
     "$SRC_DIR/sqlite3.c" -o "$SRC_DIR/sqlite3.o"
 
-wasm32posix-ar rcs "$SRC_DIR/libsqlite3.a" "$SRC_DIR/sqlite3.o"
+"${TOOL_PREFIX}-ar" rcs "$SRC_DIR/libsqlite3.a" "$SRC_DIR/sqlite3.o"
 
 # --- Install library into INSTALL_DIR ---
 echo "==> Installing to $INSTALL_DIR..."
@@ -98,7 +133,7 @@ if [ "$BUILD_CLI" = "1" ]; then
     echo "==> Building sqlite3 CLI..."
     mkdir -p "$INSTALL_DIR/bin"
     # shellcheck disable=SC2086
-    wasm32posix-cc $SQLITE_CFLAGS \
+    "${TOOL_PREFIX}-cc" $SQLITE_CFLAGS \
         "$SRC_DIR/shell.c" "$SRC_DIR/sqlite3.c" \
         -o "$INSTALL_DIR/bin/sqlite3.wasm" -lm
 
diff --git a/packages/registry/xz/build-xz.sh b/packages/registry/xz/build-xz.sh
index 92da5fe760..88604d06db 100755
--- a/packages/registry/xz/build-xz.sh
+++ b/packages/registry/xz/build-xz.sh
@@ -1,42 +1,73 @@
 #!/usr/bin/env bash
 set -euo pipefail
 
-# Build XZ Utils 5.6.4 for wasm32-posix-kernel.
+# Build XZ Utils for wasm32-posix-kernel.
 #
 # Uses the SDK's wasm32posix-configure wrapper for cross-compilation.
 # --disable-threads is critical (no pthreads support).
-# Output: packages/registry/xz/bin/xz.wasm
-# Also installs liblzma.a + headers to sysroot.
+# Output: xz.wasm. Resolver/Homebrew invocations install only the declared
+# program output into WASM_POSIX_DEP_OUT_DIR; direct legacy invocations still
+# populate packages/registry/xz/bin and sysroot.
 
-XZ_VERSION="${XZ_VERSION:-5.6.4}"
 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
 REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
-SRC_DIR="$SCRIPT_DIR/xz-src"
-BIN_DIR="$SCRIPT_DIR/bin"
-SYSROOT="$REPO_ROOT/sysroot"
+source "$REPO_ROOT/sdk/activate.sh"
+
+XZ_VERSION="${WASM_POSIX_DEP_VERSION:-${XZ_VERSION:-5.6.2}}"
+SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://tukaani.org/xz/xz-${XZ_VERSION}.tar.xz}"
+SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-}"
+WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}"
+SRC_DIR="$WORK_DIR/xz-src"
+BIN_DIR="$WORK_DIR/bin"
+INSTALL_DIR="${WASM_POSIX_DEP_OUT_DIR:-}"
+TARGET_ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}"
+
+if [ "$TARGET_ARCH" != "wasm32" ]; then
+    echo "ERROR: xz is currently packaged for wasm32 only, got $TARGET_ARCH" >&2
+    exit 2
+fi
+
+SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}"
+export WASM_POSIX_SYSROOT="$SYSROOT"
 
 # --- Prerequisites ---
 if ! command -v wasm32posix-cc &>/dev/null; then
-    echo "ERROR: wasm32posix-cc not found. Run 'npm link' in sdk/ first." >&2
+    echo "ERROR: wasm32posix-cc not found. Run through scripts/dev-shell.sh." >&2
     exit 1
 fi
 
 if [ ! -f "$SYSROOT/lib/libc.a" ]; then
-    echo "ERROR: sysroot not found. Run: bash build.sh && bash scripts/build-musl.sh" >&2
+    echo "ERROR: sysroot not found at $SYSROOT. Run scripts/build-musl.sh first." >&2
     exit 1
 fi
 
-export WASM_POSIX_SYSROOT="$SYSROOT"
-
 # --- Download xz source ---
+expected_marker="$(printf '%s\n%s\n%s\n' "$XZ_VERSION" "$SOURCE_URL" "$SOURCE_SHA256")"
+SOURCE_MARKER="$SRC_DIR/.kandelo-xz-source"
+if [ -d "$SRC_DIR" ] && [ "$(cat "$SOURCE_MARKER" 2>/dev/null || true)" != "$expected_marker" ]; then
+    echo "==> Existing xz source does not match requested version/source; cleaning..."
+    rm -rf "$SRC_DIR"
+fi
+
 if [ ! -d "$SRC_DIR" ]; then
     echo "==> Downloading xz $XZ_VERSION..."
-    TARBALL="xz-${XZ_VERSION}.tar.gz"
-    URL="https://github.com/tukaani-project/xz/releases/download/v${XZ_VERSION}/${TARBALL}"
-    curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$URL" -o "/tmp/$TARBALL"
+    tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/kandelo-xz-src.XXXXXX")"
+    trap 'rm -rf "$tmpdir"' EXIT
+    case "$SOURCE_URL" in
+        *.tar.gz|*.tgz) TARBALL="$tmpdir/xz-${XZ_VERSION}.tar.gz" ;;
+        *.tar.xz|*.txz) TARBALL="$tmpdir/xz-${XZ_VERSION}.tar.xz" ;;
+        *) TARBALL="$tmpdir/xz-${XZ_VERSION}.tar" ;;
+    esac
+    curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$SOURCE_URL" -o "$TARBALL"
+    if [ -n "$SOURCE_SHA256" ]; then
+        echo "==> Verifying source sha256..."
+        echo "$SOURCE_SHA256  $TARBALL" | shasum -a 256 -c -
+    fi
     mkdir -p "$SRC_DIR"
-    tar xzf "/tmp/$TARBALL" -C "$SRC_DIR" --strip-components=1
-    rm "/tmp/$TARBALL"
+    tar xf "$TARBALL" -C "$SRC_DIR" --strip-components=1
+    printf '%s\n' "$expected_marker" > "$SOURCE_MARKER"
+    trap - EXIT
+    rm -rf "$tmpdir"
     echo "==> Source extracted to $SRC_DIR"
 
     # Patch: xz excludes __wasm__ from sigprocmask path, but our sysroot has it
@@ -98,21 +129,29 @@ else
     exit 1
 fi
 
-# --- Install library to sysroot ---
-echo "==> Installing liblzma.a and headers to sysroot..."
-if [ -f "$SRC_DIR/src/liblzma/.libs/liblzma.a" ]; then
-    cp "$SRC_DIR/src/liblzma/.libs/liblzma.a" "$SYSROOT/lib/"
-    mkdir -p "$SYSROOT/include/lzma"
-    cp "$SRC_DIR/src/liblzma/api/lzma.h" "$SYSROOT/include/"
-    cp "$SRC_DIR/src/liblzma/api/lzma/"*.h "$SYSROOT/include/lzma/"
-    echo "==> Installed liblzma.a and headers"
+if [ -n "$INSTALL_DIR" ]; then
+    source "$REPO_ROOT/scripts/wasm-artifact-guards.sh"
+    wasm_require_no_legacy_asyncify "$BIN_DIR/xz.wasm"
+    wasm_require_no_fork_instrumentation "$BIN_DIR/xz.wasm"
+    rm -rf "$INSTALL_DIR"
+    mkdir -p "$INSTALL_DIR"
+    cp "$BIN_DIR/xz.wasm" "$INSTALL_DIR/xz.wasm"
+    echo "==> Installed xz.wasm to $INSTALL_DIR"
+else
+    # --- Install library to sysroot (legacy direct invocation only) ---
+    echo "==> Installing liblzma.a and headers to sysroot..."
+    if [ -f "$SRC_DIR/src/liblzma/.libs/liblzma.a" ]; then
+        cp "$SRC_DIR/src/liblzma/.libs/liblzma.a" "$SYSROOT/lib/"
+        mkdir -p "$SYSROOT/include/lzma"
+        cp "$SRC_DIR/src/liblzma/api/lzma.h" "$SYSROOT/include/"
+        cp "$SRC_DIR/src/liblzma/api/lzma/"*.h "$SYSROOT/include/lzma/"
+        echo "==> Installed liblzma.a and headers"
+    fi
+
+    source "$REPO_ROOT/scripts/install-local-binary.sh"
+    install_local_binary xz "$SCRIPT_DIR/bin/xz.wasm"
 fi
 
 echo ""
 echo "==> xz built successfully!"
 echo "Binary: $BIN_DIR/xz.wasm"
-
-# Install into local-binaries/ so the resolver picks the freshly-built
-# binary over the fetched release.
-source "$REPO_ROOT/scripts/install-local-binary.sh"
-install_local_binary xz "$SCRIPT_DIR/bin/xz.wasm"
diff --git a/scripts/dev-shell.sh b/scripts/dev-shell.sh
index 4ecf73cee7..57db26f350 100755
--- a/scripts/dev-shell.sh
+++ b/scripts/dev-shell.sh
@@ -118,6 +118,7 @@ nix_develop=(
     --keep KANDELO_HOMEBREW_BOTTLE_URL \
     --keep KANDELO_HOMEBREW_BOTTLE_SHA256 \
     --keep KANDELO_HOMEBREW_BOTTLE_BYTES \
+    --keep KANDELO_HOMEBREW_BROWSER_SMOKE_SUMMARY \
     --keep KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS \
     --keep KANDELO_HOMEBREW_VFS_IMAGE \
     --keep KANDELO_HOMEBREW_VFS_REPORT \
diff --git a/scripts/homebrew-bottle-build.sh b/scripts/homebrew-bottle-build.sh
index c418f9c82a..3c3a4d2fc7 100755
--- a/scripts/homebrew-bottle-build.sh
+++ b/scripts/homebrew-bottle-build.sh
@@ -85,9 +85,10 @@ fi
 KANDELO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
 PATCH_FILE="$KANDELO_ROOT/homebrew/patches/0001-add-kandelo-wasm-bottle-tags.patch"
 mkdir -p "$OUT_DIR/bottles"
-WORK_DIR="$(mktemp -d)"
+WORK_DIR="$(mktemp -d "$OUT_DIR/homebrew-work.XXXXXX")"
 BREW_REPO=""
 BREW_OVERLAY=""
+TAP_SOURCE="$TAP_ROOT"
 
 cleanup() {
   if [ -n "$BREW_REPO" ] && [ -n "$BREW_OVERLAY" ] && [ -d "$BREW_OVERLAY" ]; then
@@ -120,7 +121,18 @@ export HOMEBREW_KANDELO_ROOT="$KANDELO_ROOT"
 export HOMEBREW_KANDELO_NODE="$(command -v node)"
 export HOMEBREW_KANDELO_LLVM_BIN="${LLVM_BIN:-${WASM_POSIX_LLVM_DIR:-}}"
 
-"$BREW_BIN" tap "$TAP_NAME" "$TAP_ROOT"
+if [ ! -d "$TAP_SOURCE/.git" ]; then
+  TAP_SOURCE="$WORK_DIR/tap-source"
+  mkdir -p "$TAP_SOURCE"
+  rsync -a --exclude .git "$TAP_ROOT/" "$TAP_SOURCE/"
+  git -C "$TAP_SOURCE" init -q
+  git -C "$TAP_SOURCE" config user.name "kandelo-homebrew-local"
+  git -C "$TAP_SOURCE" config user.email "kandelo-homebrew-local@example.invalid"
+  git -C "$TAP_SOURCE" add .
+  git -C "$TAP_SOURCE" commit -q -m "stage Kandelo Homebrew tap"
+fi
+
+"$BREW_BIN" tap "$TAP_NAME" "$TAP_SOURCE"
 FORMULA_REF="$TAP_NAME/$FORMULA"
 TAPPED_TAP_ROOT="$("$BREW_BIN" --repository "$TAP_NAME")"
 TAPPED_FORMULA_PATH="$TAPPED_TAP_ROOT/Formula/$FORMULA.rb"
@@ -189,7 +201,7 @@ BOTTLE_JSON="$OUT_DIR/bottles/$(basename "${bottle_jsons[0]}")"
 BOTTLE_ARCHIVE="$OUT_DIR/bottles/$(basename "${bottle_archives[0]}")"
 
 (
-  cd "$TAP_ROOT"
+  cd "$TAPPED_TAP_ROOT"
   HOMEBREW_KANDELO_BOTTLE_TAG="$BOTTLE_TAG" \
   KANDELO_HOMEBREW_BOTTLE_TAG="$BOTTLE_TAG" \
     "$BREW_BIN" bottle --merge --write --no-commit "$BOTTLE_JSON"
diff --git a/scripts/homebrew-generate-sidecars-from-env.sh b/scripts/homebrew-generate-sidecars-from-env.sh
index e77db39103..cf9b958b83 100755
--- a/scripts/homebrew-generate-sidecars-from-env.sh
+++ b/scripts/homebrew-generate-sidecars-from-env.sh
@@ -108,89 +108,191 @@ for dep in package_toml.get("depends_on", []):
         deps.append({"name": dep})
 
 version = str(bottle_formula["pkg_version"])
-browser_smoke_status = os.environ.get("KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS", "skipped")
-if browser_smoke_status not in {"success", "skipped"}:
-    raise SystemExit(f"invalid KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS={browser_smoke_status!r}")
+package_kind = package_toml.get("kind", "program")
+if package_kind not in {"library", "program"}:
+    raise SystemExit(f"unsupported Homebrew sidecar package kind for {formula}: {package_kind!r}")
+
+def package_links_and_env():
+    if package_kind == "library":
+        outputs = package_toml.get("outputs", {})
+        links = []
+        for key in ("headers", "libs", "pkgconfig"):
+            for rel in sorted(outputs.get(key, [])):
+                links.append({
+                    "type": "file",
+                    "source": rel,
+                    "target": rel,
+                    "mode": "0644",
+                })
+        if not links:
+            raise SystemExit(f"library formula {formula} has no declared package outputs to link")
+        return links, {}
+
+    return [
+        {"type": "symlink", "source": f"bin/{formula}", "target": f"bin/{formula}"}
+    ], {"PATH_prepend": ["bin"]}
+
+def package_fork_instrumentation():
+    outputs = package_toml.get("outputs", [])
+    if isinstance(outputs, list):
+        for output in outputs:
+            if output.get("name") == formula:
+                return output.get("fork_instrumentation", "not-required")
+    return "not-required"
+
+def default_node_smoke_text():
+    if package_kind == "library":
+        return (
+            f"Formula test compiled packages/registry/{formula}/test/{formula}_basic.c "
+            "against the installed keg and ran the resulting Wasm through "
+            "node --import tsx/esm examples/run-example.ts"
+        )
+    if formula == "bzip2":
+        return (
+            "Formula test ran bzip2 --help through "
+            "node --import tsx/esm examples/run-example.ts"
+        )
+    return (
+        f"Formula test ran {formula} --version through "
+        "node --import tsx/esm examples/run-example.ts"
+    )
+
+def skipped_outcome(name, reason):
+    return {
+        "name": name,
+        "status": "skipped",
+        "passed": [],
+        "failed": [],
+        "skipped": [reason],
+        "skip_reason": reason,
+    }
+
+def failed_outcome(name, reason):
+    return {
+        "name": name,
+        "status": "failed",
+        "passed": [],
+        "failed": [reason],
+        "skipped": [],
+    }
+
+def browser_outcome_from_summary(summary_path, formula, arch):
+    with pathlib.Path(summary_path).open("r", encoding="utf-8") as f:
+        summary = json.load(f)
+    packages = summary.get("packages")
+    if not isinstance(packages, list):
+        raise SystemExit(f"browser smoke summary lacks packages list: {summary_path}")
+    matches = [
+        package for package in packages
+        if package.get("formula") == formula and package.get("arch") == arch
+    ]
+    if len(matches) != 1:
+        raise SystemExit(
+            f"browser smoke summary expected one {formula} {arch} entry, got {len(matches)}"
+        )
+    package = matches[0]
+    status = package.get("status")
+    if status not in {"success", "failed", "skipped"}:
+        raise SystemExit(f"invalid browser smoke summary status for {formula} {arch}: {status!r}")
+
+    def strings(key):
+        value = package.get(key, [])
+        if not isinstance(value, list):
+            raise SystemExit(f"browser smoke summary {formula} {arch} {key} must be a list")
+        out = []
+        for entry in value:
+            text = str(entry)
+            if text:
+                out.append(text)
+        return out
+
+    passed = strings("passed")
+    failed = strings("failed")
+    skipped = strings("skipped")
+    if status == "success" and not passed:
+        raise SystemExit(f"browser smoke summary success for {formula} {arch} has no passed evidence")
+    if status == "failed" and not failed:
+        failed = [f"browser smoke failed for {formula} {arch}; see {summary_path}"]
+    if status == "skipped" and not skipped:
+        skipped = [package.get("skip_reason") or f"browser smoke skipped for {formula} {arch}; see {summary_path}"]
+
+    outcome = {
+        "name": "browser_smoke",
+        "status": status,
+        "passed": passed,
+        "failed": failed,
+        "skipped": skipped,
+    }
+    if status == "skipped":
+        outcome["skip_reason"] = package.get("skip_reason") or skipped[0]
+    return status, outcome
+
+links, link_env = package_links_and_env()
+browser_summary = os.environ.get("KANDELO_HOMEBREW_BROWSER_SMOKE_SUMMARY", "")
+browser_smoke_outcome = None
+if browser_summary:
+    browser_smoke_status, browser_smoke_outcome = browser_outcome_from_summary(
+        browser_summary,
+        formula,
+        arch,
+    )
+else:
+    browser_smoke_status = os.environ.get("KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS", "skipped")
+    if browser_smoke_status not in {"success", "skipped", "failed"}:
+        raise SystemExit(f"invalid KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS={browser_smoke_status!r}")
 browser_compatible = browser_smoke_status == "success"
 if browser_compatible and arch != "wasm32":
     raise SystemExit("browser smoke can only mark wasm32 bottles browser-compatible")
 runtime_support = ["node", "browser"] if browser_compatible else ["node"]
 
-browser_smoke_outcome = {
-    "name": "browser_smoke",
-    "status": "skipped",
-    "passed": [],
-    "failed": [],
-    "skipped": ["browser_compatible is false for this bottle"],
-    "skip_reason": "No successful browser VFS smoke was recorded for this bottle.",
-}
-vfs_image_outcome = {
-    "name": "homebrew_vfs_image",
-    "status": "skipped",
-    "passed": [],
-    "failed": [],
-    "skipped": ["precomposed browser VFS image was not built"],
-    "skip_reason": "Browser-compatible gallery publication requires kd-8ho.10 browser smoke.",
-}
-gallery_outcome = {
-    "name": "browser_gallery",
-    "status": "skipped",
-    "passed": [],
-    "failed": [],
-    "skipped": ["browser gallery assets were not generated"],
-    "skip_reason": "Gallery assets require a successful browser VFS smoke.",
-}
-if browser_compatible:
+if browser_smoke_outcome is None:
+    browser_reason = os.environ.get(
+        "KANDELO_HOMEBREW_BROWSER_SMOKE_REASON",
+        f"No successful browser VFS smoke was recorded for {formula} {arch}.",
+    )
+    browser_smoke_outcome = skipped_outcome("browser_smoke", browser_reason)
+    if browser_smoke_status == "failed":
+        browser_smoke_outcome = failed_outcome("browser_smoke", browser_reason)
+if browser_compatible and not browser_summary:
     vfs_image = os.environ.get("KANDELO_HOMEBREW_VFS_IMAGE", "")
     vfs_report = os.environ.get("KANDELO_HOMEBREW_VFS_REPORT", "")
-    gallery_root = os.environ.get("KANDELO_HOMEBREW_GALLERY_ROOT", "")
     browser_url = os.environ.get("KANDELO_HOMEBREW_BROWSER_SMOKE_URL", "")
     browser_command = os.environ.get(
         "KANDELO_HOMEBREW_BROWSER_SMOKE_COMMAND",
-        "/home/linuxbrew/.linuxbrew/bin/hello --version",
+        f"/home/linuxbrew/.linuxbrew/bin/{formula} --version",
     )
     missing = [
         name for name, value in [
             ("KANDELO_HOMEBREW_VFS_IMAGE", vfs_image),
             ("KANDELO_HOMEBREW_VFS_REPORT", vfs_report),
-            ("KANDELO_HOMEBREW_GALLERY_ROOT", gallery_root),
             ("KANDELO_HOMEBREW_BROWSER_SMOKE_URL", browser_url),
         ] if not value
     ]
     if missing:
         raise SystemExit("browser smoke success is missing env: " + ", ".join(missing))
-    browser_smoke_outcome = {
-        "name": "browser_smoke",
-        "status": "success",
-        "passed": [
-            f"Playwright chromium launched {browser_url}",
-            f"terminal command passed: {browser_command}",
-        ],
-        "failed": [],
-        "skipped": [],
-    }
-    vfs_image_outcome = {
-        "name": "homebrew_vfs_image",
-        "status": "success",
-        "passed": [
-            f"built {vfs_image}",
-            f"wrote report {vfs_report}",
-        ],
-        "failed": [],
-        "skipped": [],
-    }
-    gallery_outcome = {
-        "name": "browser_gallery",
-        "status": "success",
-        "passed": [
+    browser_passed = [
+        f"built precomposed VFS image {vfs_image}",
+        f"wrote VFS report {vfs_report}",
+        f"Playwright chromium launched {browser_url}",
+        f"terminal command passed: {browser_command}",
+    ]
+    gallery_root = os.environ.get("KANDELO_HOMEBREW_GALLERY_ROOT", "")
+    if gallery_root:
+        browser_passed.extend([
             f"generated {gallery_root}/gallery.json",
             f"generated {gallery_root}/index.toml",
             "scripts/validate-software-gallery.mjs accepted generated gallery assets",
-        ],
+        ])
+    browser_smoke_outcome = {
+        "name": "browser_smoke",
+        "status": "success",
+        "passed": browser_passed,
         "failed": [],
         "skipped": [],
     }
 
+node_smoke_text = os.environ.get("KANDELO_HOMEBREW_NODE_SMOKE_COMMAND", default_node_smoke_text())
+
 manifest = {
     "schema": 1,
     "tap_repository": os.environ["KANDELO_HOMEBREW_TAP_REPOSITORY"],
@@ -219,7 +321,7 @@ manifest = {
                     "prefix": "/home/linuxbrew/.linuxbrew",
                     "runtime_support": runtime_support,
                     "browser_compatible": browser_compatible,
-                    "fork_instrumentation": "not-required",
+                    "fork_instrumentation": package_fork_instrumentation(),
                     "status": "success",
                     "built_by": os.environ["RUN_URL"],
                     "built_at": os.environ["GENERATED_AT"],
@@ -227,11 +329,9 @@ manifest = {
                     "url": os.environ["KANDELO_HOMEBREW_BOTTLE_URL"],
                     "cache_key_sha": os.environ["CACHE_KEY_SHA"],
                     "payload_root": f"{formula}/{version}",
-                    "links": [
-                        {"type": "symlink", "source": f"bin/{formula}", "target": f"bin/{formula}"}
-                    ],
+                    "links": links,
                     "receipts": [f".brew/{formula}.rb", "INSTALL_RECEIPT.json"],
-                    "env": {"PATH_prepend": ["bin"]},
+                    "env": link_env,
                     "build": {
                         "github_run": os.environ["RUN_URL"],
                         "job": os.environ.get("GITHUB_JOB", "local"),
@@ -260,8 +360,8 @@ manifest = {
                                 "status": "skipped",
                                 "passed": [],
                                 "failed": [],
-                                "skipped": ["brew audit was not part of kd-8ho.5 local verification"],
-                                "skip_reason": "kd-8ho.5 validates the first bottle build and sidecars; tap audit can run in the real tap publication gate.",
+                                "skipped": ["brew audit was not part of this local dry-run verification"],
+                                "skip_reason": "Tap audit can run in the trusted tap publication gate.",
                             },
                             {
                                 "name": "bottle_build",
@@ -278,15 +378,11 @@ manifest = {
                             {
                                 "name": "node_smoke",
                                 "status": "success",
-                                "passed": [
-                                    "Formula test ran hello --version through node --import tsx/esm examples/run-example.ts"
-                                ],
+                                "passed": [node_smoke_text],
                                 "failed": [],
                                 "skipped": [],
                             },
-                            vfs_image_outcome,
                             browser_smoke_outcome,
-                            gallery_outcome,
                         ],
                     },
                 }
diff --git a/scripts/homebrew-package-browser-smoke.ts b/scripts/homebrew-package-browser-smoke.ts
new file mode 100644
index 0000000000..c155074ab2
--- /dev/null
+++ b/scripts/homebrew-package-browser-smoke.ts
@@ -0,0 +1,897 @@
+/**
+ * Browser smoke coverage for Kandelo Homebrew package sidecars.
+ *
+ * The runner consumes generated Kandelo/Homebrew sidecars, materializes each
+ * requested wasm32 package into a candidate VFS without requiring existing
+ * browser-compatible metadata, boots that image through BrowserKernel in
+ * Chromium, and runs package-specific checks.
+ */
+import { execFileSync, spawn, type ChildProcess } from "node:child_process";
+import {
+  appendFileSync,
+  copyFileSync,
+  existsSync,
+  mkdirSync,
+  readFileSync,
+  renameSync,
+  rmSync,
+  writeFileSync,
+} from "node:fs";
+import { basename, dirname, join, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+import type { Browser } from "playwright";
+import { ABI_VERSION } from "../host/src/generated/abi";
+import type {
+  HomebrewBottleArch,
+  HomebrewTapMetadata,
+  HomebrewVfsPackagePlan,
+} from "../host/src/homebrew-vfs-planner";
+import type { MemoryFileSystem } from "../host/src/vfs/memory-fs";
+import {
+  HOMEBREW_CELLAR,
+  browserSmokeCasesForFormula,
+  browserUnsupportedReason,
+  parseHomebrewSmokeFormula,
+  SQLITE_BROWSER_CONSUMER_PATH,
+  type BrowserSmokeCase,
+  type HomebrewSmokeFormula,
+} from "./homebrew-package-smoke-cases";
+import {
+  countOutcomes,
+  SkipCase,
+  type SmokeOutcome,
+  writeOutcomeLists,
+} from "./homebrew-smoke-outcomes";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const repoRoot = resolve(__dirname, "..");
+const browserDemoDir = join(repoRoot, "apps", "browser-demos");
+const publicSmokeRoot = join(browserDemoDir, "public", "__kandelo-homebrew-smoke");
+const publicSmokePath = "/__kandelo-homebrew-smoke";
+
+interface CliOptions {
+  resultDir: string;
+  tapRoot: string;
+  formulas: HomebrewSmokeFormula[];
+  arch: HomebrewBottleArch;
+  bottleCache: string;
+  timeoutMs: number;
+  maxBytes: number;
+  beadId: string;
+  port: number;
+  runId: string;
+  browserChannel: string;
+}
+
+interface BuiltBrowserVfs {
+  formula: HomebrewSmokeFormula;
+  fs: MemoryFileSystem;
+  imagePath: string;
+  reportPath: string;
+  publicPath: string;
+  publicUrl: string;
+}
+
+interface BrowserDiagnostics {
+  console: string[];
+  pageErrors: string[];
+  requestFailures: string[];
+}
+
+interface BrowserSmokeResult {
+  exitCode: number;
+  stdout: string;
+  stderr: string;
+  combined: string;
+  durationMs: number;
+}
+
+type PackageStatus = "success" | "failed" | "skipped";
+
+async function main(): Promise {
+  const options = parseArgs(process.argv.slice(2));
+  mkdirSync(options.resultDir, { recursive: true });
+  mkdirSync(join(options.resultDir, "outcome-lists"), { recursive: true });
+  mkdirSync(options.bottleCache, { recursive: true });
+
+  const unsupportedReason = browserUnsupportedReason(options.arch);
+  const metadataPath = join(options.tapRoot, "Kandelo", "metadata.json");
+  const metadata = unsupportedReason ? undefined : readJsonFile(metadataPath);
+  const tapCommit = tryGitRevParse(options.tapRoot) ?? metadata?.tap_commit ?? "unknown";
+  const startedAt = new Date();
+  const outcomes: SmokeOutcome[] = [];
+  const builtByFormula = new Map();
+  const publicRunDir = join(publicSmokeRoot, options.runId);
+
+  writeCurrentRun(options, {
+    status: "running",
+    startedAt,
+    tapCommit,
+    outcomes,
+    currentCase: "startup",
+  });
+
+  try {
+    if (unsupportedReason) {
+      for (const formula of options.formulas) {
+        await runCase(outcomes, options, tapCommit, `browser_smoke_${formula}_unsupported_arch`, async () => {
+          throw new SkipCase(unsupportedReason);
+        }, formulaDir(options, formula));
+      }
+    } else {
+      mkdirSync(publicRunDir, { recursive: true });
+      for (const formula of options.formulas) {
+        await runCase(outcomes, options, tapCommit, `homebrew_browser_vfs_build_${formula}`, async () => {
+          if (!metadata) throw new Error(`missing Homebrew metadata: ${metadataPath}`);
+          const built = await buildFormulaVfs(metadata, formula, options);
+          builtByFormula.set(formula, built);
+          return `image=${built.imagePath}; report=${built.reportPath}; browser_url=${built.publicUrl}`;
+        }, formulaDir(options, formula));
+      }
+
+      let vite: ChildProcess | undefined;
+      let browser: Browser | undefined;
+      await runCase(outcomes, options, tapCommit, "browser_server_start", async () => {
+        vite = await startViteServer(options);
+        const { chromium } = await import("playwright");
+        browser = await chromium.launch({
+          channel: options.browserChannel,
+          headless: true,
+        });
+        return `vite=http://127.0.0.1:${options.port}; channel=${options.browserChannel}`;
+      }, join(options.resultDir, "vite.log"));
+
+      try {
+        for (const formula of options.formulas) {
+          for (const smokeCase of browserSmokeCasesForFormula(formula)) {
+            await runCase(
+              outcomes,
+              options,
+              tapCommit,
+              `browser_smoke_${formula}_${smokeCase.name}`,
+              async () => {
+                const built = builtByFormula.get(formula);
+                if (!built) throw new SkipCase(`requires successful homebrew_browser_vfs_build_${formula}`);
+                if (!browser) throw new Error("browser did not start");
+                return runBrowserSmokeCase(browser, built, smokeCase, options);
+              },
+              join(formulaDir(options, formula), `${smokeCase.name}-terminal.txt`),
+            );
+          }
+        }
+      } finally {
+        await browser?.close().catch(() => {});
+        await stopProcess(vite);
+      }
+    }
+  } finally {
+    rmSync(publicRunDir, { recursive: true, force: true });
+  }
+
+  writeOutcomeLists(options.resultDir, outcomes, { includeArtifactPath: true });
+  writeSummary(options, {
+    startedAt,
+    completedAt: new Date(),
+    tapCommit,
+    outcomes,
+    builtByFormula,
+  });
+  writeCurrentRun(options, {
+    status: outcomes.some((outcome) => outcome.status === "fail") ? "failed" : "complete",
+    startedAt,
+    tapCommit,
+    outcomes,
+    currentCase: "complete",
+  });
+
+  process.exit(outcomes.some((outcome) => outcome.status === "fail") ? 1 : 0);
+}
+
+async function buildFormulaVfs(
+  metadata: HomebrewTapMetadata,
+  formula: HomebrewSmokeFormula,
+  options: CliOptions,
+): Promise {
+  const [
+    { buildHomebrewVfs },
+    { planHomebrewVfs },
+    { MemoryFileSystem },
+    { saveImage },
+  ] = await Promise.all([
+    import("../host/src/homebrew-vfs-builder"),
+    import("../host/src/homebrew-vfs-planner"),
+    import("../host/src/vfs/memory-fs"),
+    import("../images/vfs/scripts/vfs-image-helpers"),
+  ]);
+  const plan = await planHomebrewVfs(metadata, {
+    packages: [formula],
+    arch: options.arch,
+    expectedAbi: ABI_VERSION,
+    loadLinkManifest: (relPath) => readJsonFile(join(options.tapRoot, relPath)),
+  });
+  const fs = createFs(MemoryFileSystem, options.maxBytes);
+  const result = await buildHomebrewVfs(plan, {
+    fs,
+    createdBy: "scripts/homebrew-package-browser-smoke.ts",
+    loadBottleBytes: (pkg) => loadBottleBytes(pkg, options),
+  });
+
+  if (formula === "sqlite") {
+    await compileAndInjectSqliteConsumer(fs, options);
+  }
+
+  const dir = formulaDir(options, formula);
+  mkdirSync(dir, { recursive: true });
+  const reportPath = join(dir, `${formula}-${options.arch}-homebrew-vfs-report.json`);
+  const imagePath = join(dir, `${formula}-${options.arch}-homebrew.vfs.zst`);
+  writeFileSync(reportPath, `${JSON.stringify({ ...result.report, image: imagePath }, null, 2)}\n`);
+  await saveImage(fs, imagePath, {
+    metadata: {
+      version: 1,
+      kernelAbi: plan.kandeloAbi,
+      createdBy: "scripts/homebrew-package-browser-smoke.ts",
+      homebrew: {
+        tapRepository: plan.tapRepository,
+        tapCommit: plan.tapCommit,
+        releaseTag: plan.releaseTag,
+        packages: plan.packages.map((pkg) => ({
+          name: pkg.name,
+          version: pkg.version,
+          arch: pkg.arch,
+          sourceStatus: pkg.sourceStatus,
+          cacheKeySha: pkg.cacheKeySha,
+        })),
+      },
+    },
+  });
+
+  const publicDir = join(publicSmokeRoot, options.runId);
+  mkdirSync(publicDir, { recursive: true });
+  const publicPath = join(publicDir, basename(imagePath));
+  copyFileSync(imagePath, publicPath);
+  const publicUrl = `http://127.0.0.1:${options.port}${publicSmokePath}/${encodeURIComponent(options.runId)}/${encodeURIComponent(basename(imagePath))}`;
+
+  return { formula, fs, imagePath, reportPath, publicPath, publicUrl };
+}
+
+async function compileAndInjectSqliteConsumer(fs: MemoryFileSystem, options: CliOptions): Promise {
+  const { ensureDirRecursive, writeVfsBinary } = await import("../images/vfs/scripts/vfs-image-helpers");
+  const stage = join(formulaDir(options, "sqlite"), "sqlite-consumer-build", options.arch);
+  rmSync(stage, { recursive: true, force: true });
+  mkdirSync(join(stage, "include"), { recursive: true });
+  mkdirSync(join(stage, "lib"), { recursive: true });
+
+  const version = findPackageVersion(fs, "sqlite");
+  writeFileSync(
+    join(stage, "include", "sqlite3.h"),
+    readVfsFile(fs, `${HOMEBREW_CELLAR}/sqlite/${version}/include/sqlite3.h`),
+  );
+  writeFileSync(
+    join(stage, "include", "sqlite3ext.h"),
+    readVfsFile(fs, `${HOMEBREW_CELLAR}/sqlite/${version}/include/sqlite3ext.h`),
+  );
+  writeFileSync(
+    join(stage, "lib", "libsqlite3.a"),
+    readVfsFile(fs, `${HOMEBREW_CELLAR}/sqlite/${version}/lib/libsqlite3.a`),
+  );
+
+  const cc = join(repoRoot, "sdk", "bin", `${options.arch}posix-cc`);
+  if (!existsSync(cc)) {
+    throw new SkipCase(`sqlite consumer compiler is unavailable: ${cc}`);
+  }
+  const testSrc = join(repoRoot, "packages", "registry", "sqlite", "test", "sqlite_basic.c");
+  const outWasm = join(stage, "sqlite_basic.wasm");
+  try {
+    execFileSync(cc, [
+      `-I${join(stage, "include")}`,
+      testSrc,
+      join(stage, "lib", "libsqlite3.a"),
+      "-lm",
+      "-o",
+      outWasm,
+    ], {
+      cwd: repoRoot,
+      env: {
+        ...process.env,
+        PATH: `${join(repoRoot, "sdk", "bin")}:${process.env.PATH ?? ""}`,
+        WASM_POSIX_SYSROOT: join(repoRoot, "sysroot"),
+      },
+      stdio: "pipe",
+    });
+  } catch (err) {
+    const message = err instanceof Error ? err.message : String(err);
+    const stderr = readProcessErrorStderr(err);
+    throw new Error(`sqlite consumer compilation failed: ${stderr || message}`);
+  }
+
+  ensureDirRecursive(fs, dirname(SQLITE_BROWSER_CONSUMER_PATH));
+  writeVfsBinary(fs, SQLITE_BROWSER_CONSUMER_PATH, new Uint8Array(readFileSync(outWasm)), 0o755);
+}
+
+async function runBrowserSmokeCase(
+  browser: Browser,
+  built: BuiltBrowserVfs,
+  smokeCase: BrowserSmokeCase,
+  options: CliOptions,
+): Promise {
+  const dir = formulaDir(options, smokeCase.formula);
+  const terminalPath = join(dir, `${smokeCase.name}-terminal.txt`);
+  const eventsPath = join(dir, `${smokeCase.name}-browser-events.json`);
+  const screenshotPath = join(dir, `${smokeCase.name}-failure.png`);
+  const tracePath = join(dir, `${smokeCase.name}-trace.zip`);
+  const diagnostics: BrowserDiagnostics = {
+    console: [],
+    pageErrors: [],
+    requestFailures: [],
+  };
+  const context = await browser.newContext();
+  await context.tracing.start({ screenshots: true, snapshots: true });
+  const page = await context.newPage();
+  page.on("console", (msg) => {
+    if (msg.type() === "warning" || msg.type() === "error") {
+      diagnostics.console.push(`[${msg.type()}] ${msg.text()}`);
+    }
+  });
+  page.on("pageerror", (err) => diagnostics.pageErrors.push(err.stack ?? err.message));
+  page.on("requestfailed", (request) => {
+    diagnostics.requestFailures.push(`${request.method()} ${request.url()} ${request.failure()?.errorText ?? ""}`.trim());
+  });
+
+  let failed = false;
+  try {
+    await page.goto(`http://127.0.0.1:${options.port}/pages/homebrew-smoke/`, {
+      waitUntil: "domcontentloaded",
+      timeout: options.timeoutMs,
+    });
+    await page.waitForFunction(() => window.__homebrewSmokeReady === true, undefined, {
+      timeout: options.timeoutMs,
+    });
+    const result = await page.evaluate(
+      async ({ vfsUrl, argv, timeoutMs }) => window.__runHomebrewSmoke({ vfsUrl, argv, timeoutMs }),
+      {
+        vfsUrl: built.publicUrl,
+        argv: smokeCase.argv,
+        timeoutMs: options.timeoutMs,
+      },
+    ) as BrowserSmokeResult;
+    const output = [
+      `command: ${smokeCase.command}`,
+      `argv: ${JSON.stringify(smokeCase.argv)}`,
+      `exitCode: ${result.exitCode}`,
+      `durationMs: ${result.durationMs}`,
+      "",
+      "stdout:",
+      result.stdout,
+      "",
+      "stderr:",
+      result.stderr,
+    ].join("\n");
+    writeFileSync(terminalPath, output);
+    writeFileSync(eventsPath, `${JSON.stringify(diagnostics, null, 2)}\n`);
+
+    if (result.exitCode !== 0) {
+      throw new Error(`${smokeCase.name} exited ${result.exitCode}; output=${JSON.stringify(output)}`);
+    }
+    smokeCase.expected.lastIndex = 0;
+    if (!smokeCase.expected.test(result.combined)) {
+      throw new Error(`${smokeCase.name} output did not match ${smokeCase.expected}: ${JSON.stringify(output)}`);
+    }
+    return `command=${smokeCase.command}; url=${built.publicUrl}; output=${terminalPath}`;
+  } catch (err) {
+    failed = true;
+    await page.screenshot({ path: screenshotPath, fullPage: true }).catch(() => {});
+    const message = err instanceof Error ? err.message : String(err);
+    writeFileSync(eventsPath, `${JSON.stringify({ ...diagnostics, error: message }, null, 2)}\n`);
+    throw err;
+  } finally {
+    await context.tracing.stop(failed ? { path: tracePath } : undefined).catch(() => {});
+    await context.close().catch(() => {});
+  }
+}
+
+async function runCase(
+  outcomes: SmokeOutcome[],
+  options: CliOptions,
+  tapCommit: string,
+  name: string,
+  fn: () => Promise,
+  artifactPath?: string,
+): Promise {
+  writeCurrentRun(options, {
+    status: "running",
+    tapCommit,
+    outcomes,
+    currentCase: name,
+  });
+  const started = Date.now();
+  try {
+    const details = await fn();
+    outcomes.push({ name, status: "pass", durationMs: Date.now() - started, details, artifactPath });
+  } catch (err) {
+    const error = err instanceof Error ? err : new Error(String(err));
+    const skipped = error instanceof SkipCase;
+    outcomes.push({
+      name,
+      status: skipped ? "skip" : "fail",
+      durationMs: Date.now() - started,
+      details: error.message,
+      ...(skipped ? {} : { error: error.stack ?? error.message }),
+      artifactPath,
+    });
+  }
+  writeCurrentRun(options, {
+    status: "running",
+    tapCommit,
+    outcomes,
+    currentCase: name,
+  });
+}
+
+async function startViteServer(options: CliOptions): Promise {
+  const logPath = join(options.resultDir, "vite.log");
+  writeFileSync(logPath, "");
+  return new Promise((resolvePromise, reject) => {
+    const proc = spawn("npx", [
+      "vite",
+      "--config",
+      join(browserDemoDir, "vite.config.ts"),
+      "--host",
+      "127.0.0.1",
+      "--port",
+      String(options.port),
+      "--strictPort",
+    ], {
+      cwd: browserDemoDir,
+      stdio: ["ignore", "pipe", "pipe"],
+      env: {
+        ...process.env,
+        KANDELO_BROWSER_TEST_NO_HMR: "1",
+        KANDELO_BROWSER_DEMO_INPUTS: "homebrew-smoke",
+        KANDELO_PLAYWRIGHT_PORT: String(options.port),
+      },
+    });
+
+    let started = false;
+    const timeout = setTimeout(() => {
+      if (!started) {
+        proc.kill();
+        reject(new Error(`Vite server did not start within 30000ms; see ${logPath}`));
+      }
+    }, 30_000);
+    const onData = (data: Buffer) => {
+      const text = data.toString();
+      appendFileSync(logPath, text);
+      if (!started && /Local:\s+http:\/\/127\.0\.0\.1:/.test(text)) {
+        started = true;
+        clearTimeout(timeout);
+        setTimeout(() => resolvePromise(proc), 500);
+      }
+    };
+    proc.stdout?.on("data", onData);
+    proc.stderr?.on("data", onData);
+    proc.on("exit", (code) => {
+      if (!started) {
+        clearTimeout(timeout);
+        reject(new Error(`Vite exited with code ${code}; see ${logPath}`));
+      }
+    });
+    proc.on("error", (err) => {
+      if (!started) {
+        clearTimeout(timeout);
+        reject(err);
+      }
+    });
+  });
+}
+
+async function stopProcess(proc: ChildProcess | undefined): Promise {
+  if (!proc || proc.exitCode !== null || proc.signalCode !== null) return;
+  await new Promise((resolvePromise) => {
+    const timer = setTimeout(() => {
+      proc.kill("SIGKILL");
+      resolvePromise();
+    }, 5_000);
+    proc.once("exit", () => {
+      clearTimeout(timer);
+      resolvePromise();
+    });
+    proc.kill();
+  });
+}
+
+async function loadBottleBytes(
+  pkg: HomebrewVfsPackagePlan,
+  options: CliOptions,
+): Promise {
+  if (pkg.url.startsWith("file://")) {
+    return new Uint8Array(readFileSync(fileURLToPath(pkg.url)));
+  }
+
+  const cachePath = join(options.bottleCache, `${pkg.sha256}.tar.gz`);
+  if (existsSync(cachePath)) return new Uint8Array(readFileSync(cachePath));
+  if (!pkg.url.startsWith("https://")) {
+    throw new Error(
+      `package ${pkg.name}@${pkg.version} bottle URL must be https:// or file://, got ${pkg.url}`,
+    );
+  }
+
+  const { fetchHomebrewBottleBytes } = await import("../host/src/homebrew-vfs-fetch");
+  const bytes = await fetchHomebrewBottleBytes(pkg.url);
+  writeFileSync(cachePath, bytes);
+  return bytes;
+}
+
+function findPackageVersion(fs: MemoryFileSystem, formula: string): string {
+  const info = JSON.parse(new TextDecoder().decode(readVfsFile(fs, "/etc/kandelo/homebrew-vfs.json")));
+  const pkg = info.packages?.find((candidate: { name?: string }) => candidate.name === formula);
+  if (!pkg) throw new Error(`package ${formula} missing from /etc/kandelo/homebrew-vfs.json`);
+  const keg = String(pkg.keg ?? "");
+  const prefix = `${HOMEBREW_CELLAR}/${formula}/`;
+  if (!keg.startsWith(prefix)) throw new Error(`unexpected ${formula} keg path: ${keg}`);
+  return keg.slice(prefix.length);
+}
+
+function readVfsFile(fs: MemoryFileSystem, path: string): Uint8Array {
+  const st = fs.stat(path);
+  const fd = fs.open(path, 0, 0);
+  try {
+    const out = new Uint8Array(st.size);
+    let offset = 0;
+    while (offset < out.byteLength) {
+      const n = fs.read(fd, out.subarray(offset), null, out.byteLength - offset);
+      if (n <= 0) break;
+      offset += n;
+    }
+    return out.subarray(0, offset);
+  } finally {
+    fs.close(fd);
+  }
+}
+
+function createFs(
+  MemoryFileSystemCtor: {
+    create(sab: SharedArrayBuffer, maxBytes?: number): MemoryFileSystem;
+  },
+  maxBytes: number,
+): MemoryFileSystem {
+  const SharedArrayBufferCtor = SharedArrayBuffer as new (
+    byteLength: number,
+    options?: { maxByteLength?: number },
+  ) => SharedArrayBuffer;
+  return MemoryFileSystemCtor.create(
+    new SharedArrayBufferCtor(maxBytes, { maxByteLength: maxBytes }),
+    maxBytes,
+  );
+}
+
+function parseArgs(args: string[]): CliOptions {
+  const defaultResultDir = join(
+    repoRoot,
+    "test-runs",
+    "homebrew-package-browser-smoke",
+    new Date().toISOString().replace(/[-:]/g, "").replace(/\..+$/, "Z"),
+  );
+  const options: CliOptions = {
+    resultDir: process.env.KANDELO_TEST_RESULT_DIR || defaultResultDir,
+    tapRoot: process.env.KANDELO_HOMEBREW_TAP_ROOT || "",
+    formulas: [],
+    arch: "wasm32",
+    bottleCache: "",
+    timeoutMs: 180_000,
+    maxBytes: 128 * 1024 * 1024,
+    beadId: process.env.KANDELO_BEAD_ID || "kd-1mr.2.1",
+    port: Number(process.env.KANDELO_PLAYWRIGHT_PORT ?? 5401),
+    runId: "",
+    browserChannel: process.env.KANDELO_PLAYWRIGHT_CHANNEL || "chromium",
+  };
+
+  for (let i = 0; i < args.length; i += 1) {
+    const arg = args[i];
+    switch (arg) {
+      case "--result-dir":
+        options.resultDir = requireValue(args, ++i, arg);
+        break;
+      case "--tap-root":
+        options.tapRoot = requireValue(args, ++i, arg);
+        break;
+      case "--formula":
+        options.formulas.push(parseFormula(requireValue(args, ++i, arg)));
+        break;
+      case "--arch":
+        options.arch = parseArch(requireValue(args, ++i, arg));
+        break;
+      case "--bottle-cache":
+        options.bottleCache = requireValue(args, ++i, arg);
+        break;
+      case "--timeout-ms":
+        options.timeoutMs = parsePositiveInt(requireValue(args, ++i, arg), arg);
+        break;
+      case "--max-bytes":
+        options.maxBytes = parseByteSize(requireValue(args, ++i, arg));
+        break;
+      case "--bead-id":
+        options.beadId = requireValue(args, ++i, arg);
+        break;
+      case "--port":
+        options.port = parsePositiveInt(requireValue(args, ++i, arg), arg);
+        break;
+      case "--run-id":
+        options.runId = sanitizeToken(requireValue(args, ++i, arg));
+        break;
+      case "--browser-channel":
+        options.browserChannel = requireValue(args, ++i, arg);
+        break;
+      case "--help":
+      case "-h":
+        usage(0);
+        break;
+      default:
+        usage(2, `unexpected argument ${arg}`);
+    }
+  }
+
+  if (!options.tapRoot) usage(2, "--tap-root is required");
+  if (options.formulas.length === 0) usage(2, "at least one --formula is required");
+  const seen = new Set();
+  for (const formula of options.formulas) {
+    if (seen.has(formula)) usage(2, `duplicate --formula ${formula}`);
+    seen.add(formula);
+  }
+  options.resultDir = resolve(options.resultDir);
+  options.tapRoot = resolve(options.tapRoot);
+  options.bottleCache = options.bottleCache
+    ? resolve(options.bottleCache)
+    : join(options.resultDir, "bottle-cache");
+  options.runId ||= `${sanitizeToken(options.beadId)}-${new Date().toISOString().replace(/[^0-9TZ]/g, "")}-${process.pid}`;
+  return options;
+}
+
+function parseFormula(value: string): HomebrewSmokeFormula {
+  try {
+    return parseHomebrewSmokeFormula(value);
+  } catch (err) {
+    usage(2, err instanceof Error ? err.message : String(err));
+  }
+}
+
+function parseArch(value: string): HomebrewBottleArch {
+  if (value === "wasm32" || value === "wasm64") return value;
+  usage(2, `--arch must be wasm32 or wasm64, got ${value}`);
+}
+
+function parsePositiveInt(value: string, flag: string): number {
+  const parsed = Number(value);
+  if (!Number.isInteger(parsed) || parsed <= 0) usage(2, `${flag} must be a positive integer`);
+  return parsed;
+}
+
+function parseByteSize(value: string): number {
+  const match = /^([1-9][0-9]*)([kKmMgG]i?[bB]?|[bB])?$/.exec(value);
+  if (!match) usage(2, `--max-bytes must be a positive byte size, got ${value}`);
+  const amount = Number(match[1]);
+  const suffix = (match[2] ?? "b").toLowerCase();
+  const multiplier = suffix.startsWith("g") ? 1024 ** 3
+    : suffix.startsWith("m") ? 1024 ** 2
+    : suffix.startsWith("k") ? 1024
+    : 1;
+  return amount * multiplier;
+}
+
+function requireValue(args: string[], index: number, flag: string): string {
+  const value = args[index];
+  if (!value || value.startsWith("--")) usage(2, `${flag} requires a value`);
+  return value;
+}
+
+function usage(code: number, message?: string): never {
+  if (message) console.error(`homebrew-package-browser-smoke: ${message}`);
+  console.error(`usage: npx tsx scripts/homebrew-package-browser-smoke.ts \\
+  --tap-root  --formula  [--formula ...] \\
+  [--arch ] [--result-dir ] [--bottle-cache ] \\
+  [--timeout-ms ] [--max-bytes ] [--port ]`);
+  process.exit(code);
+}
+
+function writeSummary(
+  options: CliOptions,
+  data: {
+    startedAt: Date;
+    completedAt: Date;
+    tapCommit: string;
+    outcomes: SmokeOutcome[];
+    builtByFormula: Map;
+  },
+): void {
+  const counts = countOutcomes(data.outcomes);
+  const packages = options.formulas.map((formula) =>
+    summarizePackage(formula, options, data.outcomes, data.builtByFormula.get(formula))
+  );
+  const summary = {
+    suite: "Homebrew package browser VFS smoke",
+    bead_id: options.beadId,
+    started_at: data.startedAt.toISOString(),
+    completed_at: data.completedAt.toISOString(),
+    duration_ms: data.completedAt.getTime() - data.startedAt.getTime(),
+    result_dir: options.resultDir,
+    tap_root: options.tapRoot,
+    tap_commit: data.tapCommit,
+    arch: options.arch,
+    formulas: options.formulas,
+    counts,
+    outcomes: data.outcomes,
+    packages,
+    artifacts: {
+      passed: join(options.resultDir, "outcome-lists", "passed-tests.tsv"),
+      failed: join(options.resultDir, "outcome-lists", "failed-tests.tsv"),
+      skipped: join(options.resultDir, "outcome-lists", "skipped-tests.tsv"),
+      failures: join(options.resultDir, "failures.json"),
+      current_run: join(options.resultDir, "current-run.json"),
+    },
+  };
+  writeFileSync(join(options.resultDir, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`);
+  writeFileSync(join(options.resultDir, "summary.md"), [
+    "# Homebrew package browser VFS smoke",
+    "",
+    `Result dir: \`${options.resultDir}\``,
+    `Tap commit: \`${data.tapCommit}\``,
+    `Counts: ${counts.pass} pass, ${counts.fail} fail, ${counts.skip} skip`,
+    "",
+    "| Formula | Status | Browser URL | Details |",
+    "|---|---:|---|---|",
+    ...packages.map((pkg) =>
+      `| \`${pkg.formula}\` | ${pkg.status} | ${pkg.browser_url ? `\`${pkg.browser_url}\`` : ""} | ${[...pkg.failed, ...pkg.skipped].join("; ").replace(/\|/g, "\\|")} |`,
+    ),
+    "",
+    "| Test | Status | Details |",
+    "|---|---:|---|",
+    ...data.outcomes.map((outcome) =>
+      `| \`${outcome.name}\` | ${outcome.status} | ${(outcome.details ?? "").replace(/\|/g, "\\|")} |`,
+    ),
+    "",
+  ].join("\n"));
+}
+
+function summarizePackage(
+  formula: HomebrewSmokeFormula,
+  options: CliOptions,
+  outcomes: SmokeOutcome[],
+  built: BuiltBrowserVfs | undefined,
+): {
+  formula: HomebrewSmokeFormula;
+  arch: HomebrewBottleArch;
+  status: PackageStatus;
+  required_cases: string[];
+  browser_url?: string;
+  vfs_image?: string;
+  vfs_report?: string;
+  commands: string[];
+  argv: string[][];
+  passed: string[];
+  failed: string[];
+  skipped: string[];
+  skip_reason?: string;
+} {
+  const smokeCases = browserSmokeCasesForFormula(formula);
+  const formulaOutcomes = outcomes.filter((outcome) =>
+    outcome.name === `homebrew_browser_vfs_build_${formula}` ||
+    outcome.name.startsWith(`browser_smoke_${formula}_`)
+  );
+  const globalOutcomes = outcomes.filter((outcome) => outcome.name === "browser_server_start");
+  const relevant = [...globalOutcomes, ...formulaOutcomes];
+  const failed = relevant.filter((outcome) => outcome.status === "fail").map(formatOutcome);
+  const skipped = formulaOutcomes.filter((outcome) => outcome.status === "skip").map(formatOutcome);
+  const passed = relevant.filter((outcome) => outcome.status === "pass").map(formatOutcome);
+  const status: PackageStatus = failed.length > 0 ? "failed" : skipped.length > 0 ? "skipped" : "success";
+  const firstSkip = formulaOutcomes.find((outcome) => outcome.status === "skip");
+
+  return {
+    formula,
+    arch: options.arch,
+    status,
+    required_cases: smokeCases.filter((smokeCase) => smokeCase.required).map((smokeCase) => smokeCase.name),
+    browser_url: built?.publicUrl,
+    vfs_image: built?.imagePath,
+    vfs_report: built?.reportPath,
+    commands: smokeCases.map((smokeCase) => smokeCase.command),
+    argv: smokeCases.map((smokeCase) => smokeCase.argv),
+    passed,
+    failed,
+    skipped,
+    ...(status === "skipped" && firstSkip?.details ? { skip_reason: firstSkip.details } : {}),
+  };
+}
+
+function formatOutcome(outcome: SmokeOutcome): string {
+  const text = outcome.status === "fail"
+    ? outcome.error ?? outcome.details ?? ""
+    : outcome.details ?? "";
+  return `${outcome.name}: ${text}${outcome.artifactPath ? ` [${outcome.artifactPath}]` : ""}`;
+}
+
+function writeCurrentRun(
+  options: CliOptions,
+  data: {
+    status: "running" | "complete" | "failed";
+    startedAt?: Date;
+    tapCommit: string;
+    outcomes: SmokeOutcome[];
+    currentCase: string;
+  },
+): void {
+  const counts = countOutcomes(data.outcomes);
+  const currentRun = {
+    suite: "homebrew-package-browser-smoke",
+    bead_id: options.beadId,
+    worktree: repoRoot,
+    result_dir: options.resultDir,
+    status: data.status,
+    started_at: data.startedAt?.toISOString(),
+    updated_at: new Date().toISOString(),
+    current_case: data.currentCase,
+    progress: {
+      completed: data.outcomes.length,
+      total: options.arch === "wasm64"
+        ? options.formulas.length
+        : options.formulas.length * 2 + 1,
+      pass: counts.pass,
+      fail: counts.fail,
+      skip: counts.skip,
+    },
+    tap_root: options.tapRoot,
+    tap_commit: data.tapCommit,
+    command: {
+      cwd: repoRoot,
+      argv: process.argv,
+    },
+    outcome_lists: {
+      passed: join(options.resultDir, "outcome-lists", "passed-tests.tsv"),
+      failed: join(options.resultDir, "outcome-lists", "failed-tests.tsv"),
+      skipped: join(options.resultDir, "outcome-lists", "skipped-tests.tsv"),
+    },
+    stale_no_runner_threshold_seconds: 900,
+    expected_next: data.status === "running"
+      ? { deterministic: true, action: "continue current browser smoke case" }
+      : { deterministic: false, action: "suite terminal" },
+  };
+  const out = join(options.resultDir, "current-run.json");
+  const tmp = `${out}.tmp`;
+  writeFileSync(tmp, `${JSON.stringify(currentRun, null, 2)}\n`);
+  renameSync(tmp, out);
+}
+
+function formulaDir(options: CliOptions, formula: HomebrewSmokeFormula): string {
+  return join(options.resultDir, `${formula}-${options.arch}`);
+}
+
+function readJsonFile(path: string): T {
+  return JSON.parse(readFileSync(path, "utf8")) as T;
+}
+
+function tryGitRevParse(path: string): string | undefined {
+  try {
+    return execFileSync("git", ["-C", path, "rev-parse", "HEAD"], {
+      encoding: "utf8",
+      stdio: ["ignore", "pipe", "ignore"],
+    }).trim();
+  } catch {
+    return undefined;
+  }
+}
+
+function readProcessErrorStderr(err: unknown): string {
+  if (err && typeof err === "object" && "stderr" in err) {
+    const stderr = (err as { stderr?: unknown }).stderr;
+    if (stderr instanceof Buffer) return stderr.toString();
+    if (typeof stderr === "string") return stderr;
+  }
+  return "";
+}
+
+function sanitizeToken(value: string): string {
+  return value.replace(/[^A-Za-z0-9_.-]+/g, "_").replace(/^_+|_+$/g, "") || "run";
+}
+
+main().catch((err) => {
+  console.error(err);
+  process.exit(1);
+});
diff --git a/scripts/homebrew-package-node-smoke.ts b/scripts/homebrew-package-node-smoke.ts
new file mode 100644
index 0000000000..938305148d
--- /dev/null
+++ b/scripts/homebrew-package-node-smoke.ts
@@ -0,0 +1,595 @@
+/**
+ * Node-side smoke coverage for Kandelo Homebrew package sidecars.
+ *
+ * The runner consumes generated Kandelo/Homebrew sidecars, materializes each
+ * requested package into a VFS, and runs a package-specific smoke through
+ * NodeKernelHost. Program packages execute their poured binary. SQLite
+ * compiles a test-only consumer from the poured headers and static library.
+ */
+import { execFileSync } from "node:child_process";
+import {
+  existsSync,
+  mkdirSync,
+  readFileSync,
+  renameSync,
+  rmSync,
+  writeFileSync,
+} from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+import { ABI_VERSION } from "../host/src/generated/abi";
+import { fetchHomebrewBottleBytes } from "../host/src/homebrew-vfs-fetch";
+import { buildHomebrewVfs } from "../host/src/homebrew-vfs-builder";
+import {
+  planHomebrewVfs,
+  type HomebrewBottleArch,
+  type HomebrewTapMetadata,
+  type HomebrewVfsPackagePlan,
+} from "../host/src/homebrew-vfs-planner";
+import { NodeKernelHost } from "../host/src/node-kernel-host";
+import { MemoryFileSystem } from "../host/src/vfs/memory-fs";
+import { saveImage } from "../images/vfs/scripts/vfs-image-helpers";
+import {
+  countOutcomes,
+  SkipCase,
+  type SmokeOutcome as Outcome,
+  writeOutcomeLists,
+} from "./homebrew-smoke-outcomes";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const repoRoot = resolve(__dirname, "..");
+const PREFIX = "/home/linuxbrew/.linuxbrew";
+const CELLAR = `${PREFIX}/Cellar`;
+
+type FormulaName = "sqlite" | "bzip2" | "xz";
+
+interface CliOptions {
+  resultDir: string;
+  tapRoot: string;
+  formulas: FormulaName[];
+  arch: HomebrewBottleArch;
+  bottleCache: string;
+  timeoutMs: number;
+  maxBytes: number;
+  beadId: string;
+}
+
+interface BuiltVfs {
+  fs: MemoryFileSystem;
+  imageBytes: Uint8Array;
+  reportPath: string;
+}
+
+async function main(): Promise {
+  const options = parseArgs(process.argv.slice(2));
+  mkdirSync(options.resultDir, { recursive: true });
+  mkdirSync(join(options.resultDir, "outcome-lists"), { recursive: true });
+  mkdirSync(options.bottleCache, { recursive: true });
+
+  const metadataPath = join(options.tapRoot, "Kandelo", "metadata.json");
+  const metadata = readJsonFile(metadataPath);
+  const tapCommit = gitRevParse(options.tapRoot);
+  const startedAt = new Date();
+  const outcomes: Outcome[] = [];
+
+  writeCurrentRun(options, {
+    status: "running",
+    startedAt,
+    tapCommit,
+    outcomes,
+    currentCase: "startup",
+  });
+
+  const builtByFormula = new Map();
+  for (const formula of options.formulas) {
+    await runCase(outcomes, options, tapCommit, `homebrew_vfs_build_${formula}`, async () => {
+      const built = await buildFormulaVfs(metadata, formula, options);
+      builtByFormula.set(formula, built);
+      return `report=${built.reportPath}`;
+    });
+
+    await runCase(outcomes, options, tapCommit, `node_smoke_${formula}`, async () => {
+      const built = builtByFormula.get(formula);
+      if (!built) throw new SkipCase(`requires successful homebrew_vfs_build_${formula}`);
+      return await runFormulaSmoke(formula, built, options);
+    });
+  }
+
+  writeOutcomeLists(options.resultDir, outcomes);
+  writeSummary(options, {
+    startedAt,
+    completedAt: new Date(),
+    tapCommit,
+    outcomes,
+  });
+  writeCurrentRun(options, {
+    status: outcomes.some((outcome) => outcome.status === "fail") ? "failed" : "complete",
+    startedAt,
+    tapCommit,
+    outcomes,
+    currentCase: "complete",
+  });
+
+  process.exit(outcomes.some((outcome) => outcome.status === "fail") ? 1 : 0);
+}
+
+async function buildFormulaVfs(
+  metadata: HomebrewTapMetadata,
+  formula: FormulaName,
+  options: CliOptions,
+): Promise {
+  const plan = await planHomebrewVfs(metadata, {
+    packages: [formula],
+    arch: options.arch,
+    runtime: "node",
+    expectedAbi: ABI_VERSION,
+    loadLinkManifest: (relPath) => readJsonFile(join(options.tapRoot, relPath)),
+  });
+  const fs = createFs(options.maxBytes);
+  const result = await buildHomebrewVfs(plan, {
+    fs,
+    createdBy: "scripts/homebrew-package-node-smoke.ts",
+    loadBottleBytes: (pkg) => loadBottleBytes(pkg, options),
+  });
+
+  const reportPath = join(options.resultDir, `${formula}-${options.arch}-homebrew-vfs-report.json`);
+  writeFileSync(reportPath, `${JSON.stringify(result.report, null, 2)}\n`);
+  const imagePath = join(options.resultDir, `${formula}-${options.arch}-homebrew.vfs.zst`);
+  const imageBytes = await saveImage(fs, imagePath, {
+    metadata: {
+      version: 1,
+      kernelAbi: plan.kandeloAbi,
+      createdBy: "scripts/homebrew-package-node-smoke.ts",
+      homebrew: {
+        tapRepository: plan.tapRepository,
+        tapCommit: plan.tapCommit,
+        releaseTag: plan.releaseTag,
+        packages: plan.packages.map((pkg) => ({
+          name: pkg.name,
+          version: pkg.version,
+          arch: pkg.arch,
+          sourceStatus: pkg.sourceStatus,
+          cacheKeySha: pkg.cacheKeySha,
+        })),
+      },
+    },
+  });
+  return { fs, imageBytes, reportPath };
+}
+
+async function runFormulaSmoke(
+  formula: FormulaName,
+  built: BuiltVfs,
+  options: CliOptions,
+): Promise {
+  switch (formula) {
+    case "sqlite":
+      return runSqliteSmoke(built, options);
+    case "bzip2":
+      return runProgramVersionSmoke("bzip2", `${PREFIX}/bin/bzip2`, /bzip2/i, built, options, ["--help"]);
+    case "xz":
+      return runProgramVersionSmoke("xz", `${PREFIX}/bin/xz`, /xz/i, built, options);
+  }
+}
+
+async function runProgramVersionSmoke(
+  argv0: string,
+  guestPath: string,
+  expected: RegExp,
+  built: BuiltVfs,
+  options: CliOptions,
+  args: string[] = ["--version"],
+): Promise {
+  const programBytes = readVfsFile(built.fs, guestPath);
+  const result = await runWasm(programBytes, [argv0, ...args], built.imageBytes, options);
+  if (result.exitCode !== 0) {
+    throw new Error(`${argv0} ${args.join(" ")} exited ${result.exitCode}; stderr=${JSON.stringify(result.stderr)}`);
+  }
+  const combined = `${result.stdout}\n${result.stderr}`;
+  if (!expected.test(combined)) {
+    throw new Error(`unexpected ${argv0} ${args.join(" ")} output: ${JSON.stringify(combined)}`);
+  }
+  return combined.trim().split("\n").find((line) => line.trim() !== "") ?? `${argv0} ${args.join(" ")} passed`;
+}
+
+async function runSqliteSmoke(built: BuiltVfs, options: CliOptions): Promise {
+  const stage = join(options.resultDir, "sqlite-consumer-build", options.arch);
+  rmSync(stage, { recursive: true, force: true });
+  mkdirSync(join(stage, "include"), { recursive: true });
+  mkdirSync(join(stage, "lib"), { recursive: true });
+
+  const version = findPackageVersion(built.fs, "sqlite");
+  writeFileSync(
+    join(stage, "include", "sqlite3.h"),
+    readVfsFile(built.fs, `${CELLAR}/sqlite/${version}/include/sqlite3.h`),
+  );
+  writeFileSync(
+    join(stage, "include", "sqlite3ext.h"),
+    readVfsFile(built.fs, `${CELLAR}/sqlite/${version}/include/sqlite3ext.h`),
+  );
+  writeFileSync(
+    join(stage, "lib", "libsqlite3.a"),
+    readVfsFile(built.fs, `${CELLAR}/sqlite/${version}/lib/libsqlite3.a`),
+  );
+
+  const testSrc = join(repoRoot, "packages", "registry", "sqlite", "test", "sqlite_basic.c");
+  const outWasm = join(stage, "sqlite_basic.wasm");
+  const cc = join(repoRoot, "sdk", "bin", `${options.arch}posix-cc`);
+  execFileSync(cc, [
+    `-I${join(stage, "include")}`,
+    testSrc,
+    join(stage, "lib", "libsqlite3.a"),
+    "-lm",
+    "-o",
+    outWasm,
+  ], {
+    cwd: repoRoot,
+    env: {
+      ...process.env,
+      PATH: `${join(repoRoot, "sdk", "bin")}:${process.env.PATH ?? ""}`,
+      WASM_POSIX_SYSROOT: join(repoRoot, options.arch === "wasm64" ? "sysroot64" : "sysroot"),
+    },
+    stdio: "pipe",
+  });
+
+  const consumerBytes = new Uint8Array(readFileSync(outWasm));
+  const result = await runWasm(consumerBytes, ["sqlite_basic"], built.imageBytes, options);
+  if (result.exitCode !== 0) {
+    throw new Error(`sqlite_basic exited ${result.exitCode}; stderr=${JSON.stringify(result.stderr)}`);
+  }
+  if (!result.stdout.includes("PASS")) {
+    throw new Error(`sqlite_basic did not report PASS: ${JSON.stringify(result.stdout)}`);
+  }
+  return "sqlite_basic linked against poured sqlite keg and reported PASS";
+}
+
+async function runWasm(
+  programBytes: Uint8Array,
+  argv: string[],
+  rootfsImage: Uint8Array,
+  options: CliOptions,
+): Promise<{ exitCode: number; stdout: string; stderr: string }> {
+  let stdout = "";
+  let stderr = "";
+  const host = new NodeKernelHost({
+    maxWorkers: 4,
+    rootfsImage,
+    onStdout: (_pid, data) => { stdout += new TextDecoder().decode(data); },
+    onStderr: (_pid, data) => { stderr += new TextDecoder().decode(data); },
+  });
+  await host.init();
+  let timeout: ReturnType | undefined;
+  try {
+    const exitPromise = host.spawn(toArrayBuffer(programBytes), argv, {
+      env: [
+        "PATH=/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin",
+        "HOME=/tmp",
+        "TMPDIR=/tmp",
+      ],
+      cwd: "/",
+      stdin: new Uint8Array(),
+    });
+    const timeoutPromise = new Promise((_, reject) => {
+      timeout = setTimeout(
+        () => reject(new Error(`${argv[0]} timed out after ${options.timeoutMs}ms`)),
+        options.timeoutMs,
+      );
+    });
+    const exitCode = await Promise.race([exitPromise, timeoutPromise]);
+    return { exitCode, stdout, stderr };
+  } finally {
+    if (timeout) clearTimeout(timeout);
+    await host.destroy().catch(() => {});
+  }
+}
+
+async function runCase(
+  outcomes: Outcome[],
+  options: CliOptions,
+  tapCommit: string,
+  name: string,
+  fn: () => Promise,
+): Promise {
+  writeCurrentRun(options, {
+    status: "running",
+    tapCommit,
+    outcomes,
+    currentCase: name,
+  });
+  const started = Date.now();
+  try {
+    const details = await fn();
+    outcomes.push({ name, status: "pass", durationMs: Date.now() - started, details });
+  } catch (err) {
+    const error = err instanceof Error ? err : new Error(String(err));
+    outcomes.push({
+      name,
+      status: error instanceof SkipCase ? "skip" : "fail",
+      durationMs: Date.now() - started,
+      details: error.message,
+      error: error.stack ?? error.message,
+    });
+  }
+  writeOutcomeLists(options.resultDir, outcomes);
+}
+
+async function loadBottleBytes(
+  pkg: HomebrewVfsPackagePlan,
+  options: CliOptions,
+): Promise {
+  if (pkg.url.startsWith("file://")) {
+    return new Uint8Array(readFileSync(fileURLToPath(pkg.url)));
+  }
+
+  const cachePath = join(options.bottleCache, `${pkg.sha256}.tar.gz`);
+  if (existsSync(cachePath)) return new Uint8Array(readFileSync(cachePath));
+  if (!pkg.url.startsWith("https://")) {
+    throw new Error(
+      `package ${pkg.name}@${pkg.version} bottle URL must be https:// or file://, got ${pkg.url}`,
+    );
+  }
+
+  const bytes = await fetchHomebrewBottleBytes(pkg.url);
+  writeFileSync(cachePath, bytes);
+  return bytes;
+}
+
+function findPackageVersion(fs: MemoryFileSystem, formula: string): string {
+  const info = JSON.parse(new TextDecoder().decode(readVfsFile(fs, "/etc/kandelo/homebrew-vfs.json")));
+  const pkg = info.packages?.find((candidate: { name?: string }) => candidate.name === formula);
+  if (!pkg) throw new Error(`package ${formula} missing from /etc/kandelo/homebrew-vfs.json`);
+  const keg = String(pkg.keg ?? "");
+  const prefix = `${CELLAR}/${formula}/`;
+  if (!keg.startsWith(prefix)) throw new Error(`unexpected sqlite keg path: ${keg}`);
+  return keg.slice(prefix.length);
+}
+
+function readVfsFile(fs: MemoryFileSystem, path: string): Uint8Array {
+  const st = fs.stat(path);
+  const fd = fs.open(path, 0, 0);
+  try {
+    const out = new Uint8Array(st.size);
+    let offset = 0;
+    while (offset < out.byteLength) {
+      const n = fs.read(fd, out.subarray(offset), null, out.byteLength - offset);
+      if (n <= 0) break;
+      offset += n;
+    }
+    return out.subarray(0, offset);
+  } finally {
+    fs.close(fd);
+  }
+}
+
+function createFs(maxBytes: number): MemoryFileSystem {
+  const SharedArrayBufferCtor = SharedArrayBuffer as new (
+    byteLength: number,
+    options?: { maxByteLength?: number },
+  ) => SharedArrayBuffer;
+  return MemoryFileSystem.create(
+    new SharedArrayBufferCtor(maxBytes, { maxByteLength: maxBytes }),
+    maxBytes,
+  );
+}
+
+function parseArgs(args: string[]): CliOptions {
+  const defaultResultDir = join(
+    repoRoot,
+    "test-runs",
+    "homebrew-package-node-smoke",
+    new Date().toISOString().replace(/[-:]/g, "").replace(/\..+$/, "Z"),
+  );
+  const options: CliOptions = {
+    resultDir: process.env.KANDELO_TEST_RESULT_DIR || defaultResultDir,
+    tapRoot: process.env.KANDELO_HOMEBREW_TAP_ROOT || "",
+    formulas: [],
+    arch: "wasm32",
+    bottleCache: "",
+    timeoutMs: 30_000,
+    maxBytes: 128 * 1024 * 1024,
+    beadId: process.env.KANDELO_BEAD_ID || "kd-1mr.2",
+  };
+
+  for (let i = 0; i < args.length; i += 1) {
+    const arg = args[i];
+    switch (arg) {
+      case "--result-dir":
+        options.resultDir = requireValue(args, ++i, arg);
+        break;
+      case "--tap-root":
+        options.tapRoot = requireValue(args, ++i, arg);
+        break;
+      case "--formula":
+        options.formulas.push(parseFormula(requireValue(args, ++i, arg)));
+        break;
+      case "--arch":
+        options.arch = parseArch(requireValue(args, ++i, arg));
+        break;
+      case "--bottle-cache":
+        options.bottleCache = requireValue(args, ++i, arg);
+        break;
+      case "--timeout-ms":
+        options.timeoutMs = parsePositiveInt(requireValue(args, ++i, arg), arg);
+        break;
+      case "--max-bytes":
+        options.maxBytes = parseByteSize(requireValue(args, ++i, arg));
+        break;
+      case "--bead-id":
+        options.beadId = requireValue(args, ++i, arg);
+        break;
+      case "--help":
+      case "-h":
+        usage(0);
+        break;
+      default:
+        usage(2, `unexpected argument ${arg}`);
+    }
+  }
+
+  if (!options.tapRoot) usage(2, "--tap-root is required");
+  if (options.formulas.length === 0) usage(2, "at least one --formula is required");
+  options.resultDir = resolve(options.resultDir);
+  options.tapRoot = resolve(options.tapRoot);
+  options.bottleCache = options.bottleCache
+    ? resolve(options.bottleCache)
+    : join(options.resultDir, "bottle-cache");
+  return options;
+}
+
+function parseFormula(value: string): FormulaName {
+  if (value === "sqlite" || value === "bzip2" || value === "xz") return value;
+  usage(2, `--formula must be sqlite, bzip2, or xz, got ${value}`);
+}
+
+function parseArch(value: string): HomebrewBottleArch {
+  if (value === "wasm32" || value === "wasm64") return value;
+  usage(2, `--arch must be wasm32 or wasm64, got ${value}`);
+}
+
+function parsePositiveInt(value: string, flag: string): number {
+  const parsed = Number(value);
+  if (!Number.isInteger(parsed) || parsed <= 0) usage(2, `${flag} must be a positive integer`);
+  return parsed;
+}
+
+function parseByteSize(value: string): number {
+  const match = /^([1-9][0-9]*)([kKmMgG]i?[bB]?|[bB])?$/.exec(value);
+  if (!match) usage(2, `--max-bytes must be a positive byte size, got ${value}`);
+  const amount = Number(match[1]);
+  const suffix = (match[2] ?? "b").toLowerCase();
+  const multiplier = suffix.startsWith("g") ? 1024 ** 3
+    : suffix.startsWith("m") ? 1024 ** 2
+    : suffix.startsWith("k") ? 1024
+    : 1;
+  return amount * multiplier;
+}
+
+function requireValue(args: string[], index: number, flag: string): string {
+  const value = args[index];
+  if (!value || value.startsWith("--")) usage(2, `${flag} requires a value`);
+  return value;
+}
+
+function usage(code: number, message?: string): never {
+  if (message) console.error(`homebrew-package-node-smoke: ${message}`);
+  console.error(`usage: npx tsx scripts/homebrew-package-node-smoke.ts \\
+  --tap-root  --formula  [--formula ...] \\
+  [--arch ] [--result-dir ] [--bottle-cache ]`);
+  process.exit(code);
+}
+
+function writeSummary(
+  options: CliOptions,
+  data: {
+    startedAt: Date;
+    completedAt: Date;
+    tapCommit: string;
+    outcomes: Outcome[];
+  },
+): void {
+  const counts = countOutcomes(data.outcomes);
+  const summary = {
+    suite: "Homebrew package Node VFS smoke",
+    bead_id: options.beadId,
+    started_at: data.startedAt.toISOString(),
+    completed_at: data.completedAt.toISOString(),
+    duration_ms: data.completedAt.getTime() - data.startedAt.getTime(),
+    result_dir: options.resultDir,
+    tap_root: options.tapRoot,
+    tap_commit: data.tapCommit,
+    arch: options.arch,
+    formulas: options.formulas,
+    counts,
+    outcomes: data.outcomes,
+    artifacts: {
+      passed: join(options.resultDir, "outcome-lists", "passed-tests.tsv"),
+      failed: join(options.resultDir, "outcome-lists", "failed-tests.tsv"),
+      skipped: join(options.resultDir, "outcome-lists", "skipped-tests.tsv"),
+      failures: join(options.resultDir, "failures.json"),
+      current_run: join(options.resultDir, "current-run.json"),
+    },
+  };
+  writeFileSync(join(options.resultDir, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`);
+  writeFileSync(join(options.resultDir, "summary.md"), [
+    "# Homebrew package Node VFS smoke",
+    "",
+    `Result dir: \`${options.resultDir}\``,
+    `Tap commit: \`${data.tapCommit}\``,
+    `Counts: ${counts.pass} pass, ${counts.fail} fail, ${counts.skip} skip`,
+    "",
+    "| Test | Status | Details |",
+    "|---|---:|---|",
+    ...data.outcomes.map((outcome) =>
+      `| \`${outcome.name}\` | ${outcome.status} | ${outcome.details ? outcome.details.replace(/\|/g, "\\|") : ""} |`,
+    ),
+    "",
+  ].join("\n"));
+}
+
+function writeCurrentRun(
+  options: CliOptions,
+  data: {
+    status: "running" | "complete" | "failed";
+    startedAt?: Date;
+    tapCommit: string;
+    outcomes: Outcome[];
+    currentCase: string;
+  },
+): void {
+  const counts = countOutcomes(data.outcomes);
+  const currentRun = {
+    suite: "homebrew-package-node-smoke",
+    bead_id: options.beadId,
+    worktree: repoRoot,
+    result_dir: options.resultDir,
+    status: data.status,
+    started_at: data.startedAt?.toISOString(),
+    updated_at: new Date().toISOString(),
+    current_case: data.currentCase,
+    progress: {
+      completed: data.outcomes.length,
+      total: options.formulas.length * 2,
+      pass: counts.pass,
+      fail: counts.fail,
+      skip: counts.skip,
+    },
+    tap_root: options.tapRoot,
+    tap_commit: data.tapCommit,
+    command: {
+      cwd: repoRoot,
+      argv: process.argv,
+    },
+    outcome_lists: {
+      passed: join(options.resultDir, "outcome-lists", "passed-tests.tsv"),
+      failed: join(options.resultDir, "outcome-lists", "failed-tests.tsv"),
+      skipped: join(options.resultDir, "outcome-lists", "skipped-tests.tsv"),
+    },
+    stale_no_runner_threshold_seconds: 600,
+    expected_next: data.status === "running"
+      ? { deterministic: true, action: "continue current smoke case" }
+      : { deterministic: false, action: "suite terminal" },
+  };
+  const out = join(options.resultDir, "current-run.json");
+  const tmp = `${out}.tmp`;
+  writeFileSync(tmp, `${JSON.stringify(currentRun, null, 2)}\n`);
+  renameSync(tmp, out);
+}
+
+function readJsonFile(path: string): T {
+  return JSON.parse(readFileSync(path, "utf8")) as T;
+}
+
+function gitRevParse(path: string): string {
+  return execFileSync("git", ["-C", path, "rev-parse", "HEAD"], {
+    encoding: "utf8",
+  }).trim();
+}
+
+function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
+  return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
+}
+
+main().catch((err) => {
+  console.error(err);
+  process.exit(1);
+});
diff --git a/scripts/homebrew-package-smoke-cases.ts b/scripts/homebrew-package-smoke-cases.ts
new file mode 100644
index 0000000000..742e4fe9f6
--- /dev/null
+++ b/scripts/homebrew-package-smoke-cases.ts
@@ -0,0 +1,91 @@
+import type { HomebrewBottleArch } from "../host/src/homebrew-vfs-planner";
+
+export const HOMEBREW_PREFIX = "/home/linuxbrew/.linuxbrew";
+export const HOMEBREW_CELLAR = `${HOMEBREW_PREFIX}/Cellar`;
+export const SQLITE_BROWSER_CONSUMER_PATH = "/usr/local/kandelo-smoke/bin/sqlite_basic";
+
+export type HomebrewSmokeFormula = "hello" | "sqlite" | "bzip2" | "xz";
+
+export interface BrowserSmokeCase {
+  name: string;
+  formula: HomebrewSmokeFormula;
+  required: boolean;
+  command: string;
+  argv: string[];
+  expected: RegExp;
+  description: string;
+}
+
+export function parseHomebrewSmokeFormula(value: string): HomebrewSmokeFormula {
+  if (value === "hello" || value === "sqlite" || value === "bzip2" || value === "xz") {
+    return value;
+  }
+  throw new Error(`formula must be hello, sqlite, bzip2, or xz, got ${value}`);
+}
+
+export function browserUnsupportedReason(arch: HomebrewBottleArch): string | undefined {
+  return arch === "wasm64"
+    ? "wasm64 browser compatibility is unsupported by the current Homebrew browser sidecar path"
+    : undefined;
+}
+
+export function browserSmokeCasesForFormula(formula: HomebrewSmokeFormula): BrowserSmokeCase[] {
+  switch (formula) {
+    case "hello":
+      return [programOutputCase({
+        formula,
+        name: "hello_version",
+        argv: [`${HOMEBREW_PREFIX}/bin/hello`, "--version"],
+        expected: /hello/i,
+        description: "Run hello --version from the poured Homebrew prefix.",
+      })];
+    case "bzip2":
+      return [programOutputCase({
+        formula,
+        name: "bzip2_help",
+        argv: [`${HOMEBREW_PREFIX}/bin/bzip2`, "--help"],
+        expected: /bzip2/i,
+        description: "Run bzip2 --help from the poured Homebrew prefix.",
+      })];
+    case "xz":
+      return [programOutputCase({
+        formula,
+        name: "xz_version",
+        argv: [`${HOMEBREW_PREFIX}/bin/xz`, "--version"],
+        expected: /xz/i,
+        description: "Run xz --version from the poured Homebrew prefix.",
+      })];
+    case "sqlite":
+      return [{
+        name: "sqlite_basic_consumer",
+        formula,
+        required: true,
+        command: SQLITE_BROWSER_CONSUMER_PATH,
+        argv: [SQLITE_BROWSER_CONSUMER_PATH],
+        expected: /PASS/,
+        description: "Run sqlite_basic linked against the poured sqlite keg.",
+      }];
+  }
+}
+
+export function browserCaseNamesForFormula(formula: HomebrewSmokeFormula): string[] {
+  return browserSmokeCasesForFormula(formula).map((smokeCase) => smokeCase.name);
+}
+
+function programOutputCase(options: {
+  formula: HomebrewSmokeFormula;
+  name: string;
+  argv: string[];
+  expected: RegExp;
+  description: string;
+}): BrowserSmokeCase {
+  return {
+    name: options.name,
+    formula: options.formula,
+    required: true,
+    command: options.argv.join(" "),
+    argv: options.argv,
+    expected: options.expected,
+    description: options.description,
+  };
+}
diff --git a/scripts/homebrew-smoke-outcomes.ts b/scripts/homebrew-smoke-outcomes.ts
new file mode 100644
index 0000000000..63a6c59747
--- /dev/null
+++ b/scripts/homebrew-smoke-outcomes.ts
@@ -0,0 +1,86 @@
+import { mkdirSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+
+export type SmokeOutcomeStatus = "pass" | "fail" | "skip";
+
+export interface SmokeOutcome {
+  name: string;
+  status: SmokeOutcomeStatus;
+  durationMs: number;
+  details?: string;
+  error?: string;
+  artifactPath?: string;
+}
+
+export class SkipCase extends Error {
+  constructor(message: string) {
+    super(message);
+    this.name = "SkipCase";
+  }
+}
+
+export function countOutcomes(outcomes: SmokeOutcome[]): {
+  pass: number;
+  fail: number;
+  skip: number;
+} {
+  return {
+    pass: outcomes.filter((outcome) => outcome.status === "pass").length,
+    fail: outcomes.filter((outcome) => outcome.status === "fail").length,
+    skip: outcomes.filter((outcome) => outcome.status === "skip").length,
+  };
+}
+
+export function writeOutcomeLists(
+  resultDir: string,
+  outcomes: SmokeOutcome[],
+  options: { includeArtifactPath?: boolean } = {},
+): void {
+  const listsDir = join(resultDir, "outcome-lists");
+  mkdirSync(listsDir, { recursive: true });
+  const passed = outcomes.filter((outcome) => outcome.status === "pass");
+  const failed = outcomes.filter((outcome) => outcome.status === "fail");
+  const skipped = outcomes.filter((outcome) => outcome.status === "skip");
+  const withArtifact = options.includeArtifactPath === true;
+
+  writeFileSync(
+    join(listsDir, "passed-tests.tsv"),
+    [
+      withArtifact ? "test\tduration_ms\tdetails\tartifact_path" : "test\tduration_ms\tdetails",
+      ...passed.map((outcome) => [
+        outcome.name,
+        String(outcome.durationMs),
+        tsv(outcome.details ?? ""),
+        ...(withArtifact ? [tsv(outcome.artifactPath ?? "")] : []),
+      ].join("\t")),
+    ].join("\n") + "\n",
+  );
+  writeFileSync(
+    join(listsDir, "failed-tests.tsv"),
+    [
+      withArtifact ? "test\tduration_ms\terror\tartifact_path" : "test\tduration_ms\terror",
+      ...failed.map((outcome) => [
+        outcome.name,
+        String(outcome.durationMs),
+        tsv(outcome.error ?? outcome.details ?? ""),
+        ...(withArtifact ? [tsv(outcome.artifactPath ?? "")] : []),
+      ].join("\t")),
+    ].join("\n") + "\n",
+  );
+  writeFileSync(
+    join(listsDir, "skipped-tests.tsv"),
+    [
+      withArtifact ? "test\treason\tartifact_path" : "test\treason",
+      ...skipped.map((outcome) => [
+        outcome.name,
+        tsv(outcome.details ?? ""),
+        ...(withArtifact ? [tsv(outcome.artifactPath ?? "")] : []),
+      ].join("\t")),
+    ].join("\n") + "\n",
+  );
+  writeFileSync(join(resultDir, "failures.json"), `${JSON.stringify(failed, null, 2)}\n`);
+}
+
+function tsv(value: string): string {
+  return value.replace(/\t/g, " ").replace(/\r?\n/g, "\\n");
+}
diff --git a/tests/package-system/homebrew-smoke-helpers.test.ts b/tests/package-system/homebrew-smoke-helpers.test.ts
new file mode 100644
index 0000000000..f309997e38
--- /dev/null
+++ b/tests/package-system/homebrew-smoke-helpers.test.ts
@@ -0,0 +1,82 @@
+import { mkdtempSync, readFileSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { describe, expect, it } from "vitest";
+import {
+  browserCaseNamesForFormula,
+  browserSmokeCasesForFormula,
+  browserUnsupportedReason,
+  parseHomebrewSmokeFormula,
+  SQLITE_BROWSER_CONSUMER_PATH,
+} from "../../scripts/homebrew-package-smoke-cases";
+import {
+  countOutcomes,
+  writeOutcomeLists,
+  type SmokeOutcome,
+} from "../../scripts/homebrew-smoke-outcomes";
+
+describe("Homebrew package smoke helpers", () => {
+  it("plans concrete browser cases for pilot packages", () => {
+    expect(browserCaseNamesForFormula("bzip2")).toEqual(["bzip2_help"]);
+    expect(browserCaseNamesForFormula("xz")).toEqual(["xz_version"]);
+    expect(browserCaseNamesForFormula("sqlite")).toEqual(["sqlite_basic_consumer"]);
+
+    const bzip2 = browserSmokeCasesForFormula("bzip2")[0];
+    expect(bzip2.command).toContain("/home/linuxbrew/.linuxbrew/bin/bzip2 --help");
+    expect(bzip2.argv).toEqual(["/home/linuxbrew/.linuxbrew/bin/bzip2", "--help"]);
+    expect(bzip2.expected.test("bzip2, a block-sorting file compressor")).toBe(true);
+
+    const sqlite = browserSmokeCasesForFormula("sqlite")[0];
+    expect(sqlite.command).toBe(SQLITE_BROWSER_CONSUMER_PATH);
+    expect(sqlite.argv).toEqual([SQLITE_BROWSER_CONSUMER_PATH]);
+    expect(sqlite.expected.test("PASS")).toBe(true);
+  });
+
+  it("classifies supported formulas and the current wasm64 browser boundary", () => {
+    expect(parseHomebrewSmokeFormula("hello")).toBe("hello");
+    expect(parseHomebrewSmokeFormula("sqlite")).toBe("sqlite");
+    expect(() => parseHomebrewSmokeFormula("zlib")).toThrow(/formula must be/);
+    expect(browserUnsupportedReason("wasm32")).toBeUndefined();
+    expect(browserUnsupportedReason("wasm64")).toMatch(/wasm64 browser compatibility is unsupported/);
+  });
+
+  it("writes passed, failed, and skipped outcome lists with browser artifact paths", () => {
+    const dir = mkdtempSync(join(tmpdir(), "kandelo-homebrew-smoke-"));
+    try {
+      const outcomes: SmokeOutcome[] = [
+        {
+          name: "browser_smoke_bzip2_help",
+          status: "pass",
+          durationMs: 12,
+          details: "terminal command passed",
+          artifactPath: "/tmp/terminal.txt",
+        },
+        {
+          name: "browser_smoke_xz_version",
+          status: "fail",
+          durationMs: 34,
+          error: "terminal command exited 1",
+          artifactPath: "/tmp/trace.zip",
+        },
+        {
+          name: "browser_smoke_sqlite_consumer",
+          status: "skip",
+          durationMs: 0,
+          details: "sqlite consumer compiler is unavailable",
+          artifactPath: "/tmp/sqlite",
+        },
+      ];
+      writeOutcomeLists(dir, outcomes, { includeArtifactPath: true });
+
+      expect(countOutcomes(outcomes)).toEqual({ pass: 1, fail: 1, skip: 1 });
+      expect(readFileSync(join(dir, "outcome-lists", "passed-tests.tsv"), "utf8"))
+        .toContain("test\tduration_ms\tdetails\tartifact_path");
+      expect(readFileSync(join(dir, "outcome-lists", "failed-tests.tsv"), "utf8"))
+        .toContain("browser_smoke_xz_version\t34\tterminal command exited 1\t/tmp/trace.zip");
+      expect(readFileSync(join(dir, "outcome-lists", "skipped-tests.tsv"), "utf8"))
+        .toContain("browser_smoke_sqlite_consumer\tsqlite consumer compiler is unavailable\t/tmp/sqlite");
+    } finally {
+      rmSync(dir, { recursive: true, force: true });
+    }
+  });
+});