From 97568ddd4c11062840a3bbedd2abbf40a6c5bc76 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Wed, 1 Jul 2026 09:19:39 -0400 Subject: [PATCH 1/5] homebrew: research builtin bottle publishing vs custom publish path Design/research artifact for kd-1i0u (blocks kd-1mr homebrew-all). Compares Homebrew's builtin bottle automation (brew test-bot / pr-pull / pr-upload, GHCR OCI layout) against Kandelo's as-built custom publish path. Finding: the custom path already uses Homebrew's builtin bottle primitives (install --build-bottle, test, bottle --json/--merge); its orchestration, state-locked tap commit, sidecars, VFS/gallery gating, cache-key skip, and bottles-abi-v release assets have no builtin equivalent and fit Kandelo's cross-compiled-Wasm + push-to-main + VFS-consumer model. The one divergence worth converging is the GHCR upload layout: oras push is blob-addressable for the VFS fetcher but not the native version-tagged OCI layout a stock guest brew install resolves. Recommends keeping the custom path and adopting brew pr-upload --upload-only (or emitting a version-tagged OCI index) only when guest brew install becomes a goal; documents the latent blocker. Co-Authored-By: Claude Opus 4.8 --- ...in-vs-custom-bottle-publishing-research.md | 503 ++++++++++++++++++ 1 file changed, 503 insertions(+) create mode 100644 docs/plans/2026-07-01-homebrew-builtin-vs-custom-bottle-publishing-research.md diff --git a/docs/plans/2026-07-01-homebrew-builtin-vs-custom-bottle-publishing-research.md b/docs/plans/2026-07-01-homebrew-builtin-vs-custom-bottle-publishing-research.md new file mode 100644 index 000000000..83a50fd00 --- /dev/null +++ b/docs/plans/2026-07-01-homebrew-builtin-vs-custom-bottle-publishing-research.md @@ -0,0 +1,503 @@ +# Homebrew Builtin Bottle Publishing vs Kandelo Custom Publish Path + +Date: 2026-07-01 + +Tracked work: + +- `kd-1i0u` - Research: Homebrew builtin bottle publishing (`brew bottle`/`pour`, + GHCR, `test-bot`/`pr-pull`) vs Kandelo custom publish path. +- Blocks umbrella convoy `kd-1mr` - Port all current Kandelo packages to + Homebrew (initiative `homebrew-all`). + +Prior design context (already implemented, on `main` as of `1ab41fe2a`, +PR #785): + +- `docs/plans/2026-06-25-homebrew-ci-github-packages-bottle-publishing-design.md` + (`kd-7dk`/`kd-8ho`) +- `docs/plans/2026-06-27-homebrew-tap-layout-metadata-schema-design.md` + (`kd-8ho.1`) +- `docs/plans/2026-06-28-homebrew-vfs-builder-pour-link-design.md` +- `docs/homebrew-publishing.md` (reference doc for the as-built path) + +This is design/research work only. It changes no runtime behavior and adds no +code. It answers one question for `kd-1mr`: as we scale from the `hello` pilot +to all ~73 registry packages, should Kandelo keep building its own bottle +publishing automation, adopt Homebrew's builtin automation (`brew test-bot`, +`brew pr-pull`, `brew pr-upload`), or converge on some hybrid? + +## Problem Statement + +Kandelo already publishes one Homebrew bottle (`hello`, wasm32, ABI-versioned) +through a **first-party, custom-built publish path** implemented entirely in +`Automattic/kandelo`: + +- `scripts/homebrew-bottle-build.sh` - build + test + bottle one formula. +- `scripts/homebrew-ghcr-upload.sh` - push bottle bytes to GHCR. +- `scripts/homebrew-generate-sidecars-from-env.sh` + + `tools/xtask/src/homebrew_sidecars.rs` - generate Kandelo sidecar metadata. +- `scripts/homebrew-publish-sidecars.sh` - validate + commit sidecars/bottle DSL + to the tap, with a state lock and last-green fallback. +- `scripts/homebrew-plan-matrix.sh` - compute the `(formula, arch)` build matrix + with `cache_key_sha` skip logic. +- `.github/workflows/reusable-homebrew-bottle-publish.yml` - orchestrate all of + the above on `push`/`workflow_dispatch`. +- `homebrew/patches/0001-add-kandelo-wasm-bottle-tags.patch` - teach Homebrew's + bottle-tag parser about `wasm32`/`wasm64` and a `:kandelo` pseudo-system. + +Homebrew ships its own end-to-end bottle publishing automation that +`homebrew-core` and most large third-party taps use: `brew test-bot` (build + +test bottles in PR CI), `brew pr-upload` (upload bottle bytes to GHCR as OCI +images and rewrite the `bottle do` DSL), and `brew pr-pull` (pull a reviewed +PR's bottles, commit the DSL, and merge). `brew tap-new --github-packages` +scaffolds a tap wired for exactly this. + +The `homebrew-all` convoy is about to fan out across every package. Before we +multiply the surface area of the custom path, we should decide - with evidence - +which parts of it are load-bearing Kandelo-specific work and which parts +reinvent Homebrew builtins we could reuse. Reinventing what Homebrew maintains +is maintenance debt; bending Homebrew's runner-native, PR-merge automation to a +cross-compiled Wasm target it was never designed for is a different kind of +debt. This document draws that line. + +## Non-Goals + +- Do not implement or migrate anything here. This is a decision record with a + staged, optional implementation sketch for the one change it recommends. +- Do not re-open settled decisions from the 2026-06-25/06-27/06-28 designs (tap + name `Automattic/kandelo-homebrew`, sidecar schema, `bottles-abi-v` + releases, VFS builder contract, `wasm32_kandelo`/`wasm64_kandelo` tags). Those + are the baseline this compares against. +- Do not design guest `brew install` bring-up. This document identifies where + the current upload layout blocks it and what unblocks it, but the guest + install path is separate work (`docs/homebrew-publishing.md` "Current Gaps"). +- Do not revive Kandelo package-archive (`index.toml`/`.tar.zst`) publishing as + a substitute for bottles, or vice versa. They are siblings by prior decision. + +## TL;DR / Position + +**Keep the custom path for orchestration, tap commit, and sidecars. Adopt one +Homebrew builtin - the GHCR upload layout - and only when guest `brew install` +becomes a goal. Do not adopt `brew test-bot` or `brew pr-pull`.** + +The custom path is not reinvention across the board. It already uses Homebrew's +builtin bottle *primitives* (`brew install --build-bottle`, `brew test`, `brew +bottle --json`, `brew bottle --merge --write`). What it adds around them - +matrix planning with `cache_key_sha` skip, sidecar generation, VFS/gallery +gating, last-green fallback, state-locked tap commits, `bottles-abi-v` +release assets - has **no Homebrew builtin equivalent** and exists because +Kandelo bottles are cross-compiled Wasm for a synthetic `:kandelo` system, +published from `main` (not merged from PRs), and consumed by a VFS builder +rather than (yet) by `brew install`. + +`brew test-bot` and `brew pr-pull` are built on three assumptions Kandelo does +not satisfy: (1) the CI runner's own OS/arch is the bottle target; (2) bottles +are produced per PR and *merged* into the tap by pulling PR artifacts; (3) the +only metadata that matters is Homebrew's `bottle do` block. Adopting them would +mean fighting all three, for less orchestration code than we'd save. + +The **one** builtin worth converging on is `brew pr-upload`'s GHCR **layout**. +The custom `oras push` produces a blob-addressable upload that Kandelo's own VFS +fetcher can read, but it is **not** in Homebrew's native OCI layout (manifest +tagged by formula *version*), so a stock guest `brew install` cannot resolve it. +`brew pr-upload --upload-only` produces the native layout from the same +locally-built bottle bytes, decoupled from `test-bot`/`pr-pull`. Switching the +upload step to it (or teaching the custom uploader to emit the same +version-tagged OCI index) is a low-risk change that keeps every Kandelo-specific +behavior while making the published bottles installable by unmodified Homebrew. +That is the "make it more generally useful without compromising the requested +work" move, and it is the natural precondition for guest install. + +## Background: the two candidate mechanisms + +### Homebrew builtin bottle publishing (as `homebrew-core` uses it) + +Bottle **primitives** (per-formula, platform-agnostic building blocks): + +- `brew install --build-bottle ` - build a relocatable keg suitable for + bottling. +- `brew test ` - run the formula's `test do`. +- `brew bottle --json ` - package the keg into a `.bottle.tar.gz` and + emit a `*.bottle.json` describing it. +- `brew bottle --merge --write ` - splice the generated `bottle do` block + (sha256 per platform tag, `root_url`, `rebuild`, `cellar`) into the formula. + +Bottle **automation** (the end-to-end pipeline): + +- `brew test-bot` - the CI orchestrator. On a tap PR it builds and tests the + changed formulae, runs the bottle primitives, and uploads the resulting + bottle files + `*.bottle.json` as **workflow artifacts**. It determines the + platform tag from the **runner it is executing on** (an `arm64_sonoma` runner + produces an `arm64_sonoma` bottle). `brew tap-new` scaffolds a `tests.yml` + that runs it. +- `brew pr-upload [--upload-only] --root-url=` - takes locally present + bottle files + `*.bottle.json` (from a `test-bot` artifact download, or any + local build), runs `brew audit`, uploads the bottle bytes to GHCR as **OCI + images** (via `skopeo`/`oras`), and writes the final `bottle do` DSL. It can + run standalone on a local bottle directory; it does not require `test-bot`. +- `brew pr-pull --workflows=tests.yml --root-url= ` - the maintainer + merge command. Downloads a reviewed PR's `test-bot` bottle artifacts, calls + `pr-upload`, commits the DSL, and pushes to the tap's default branch (closing + the PR). This is the canonical `homebrew-core` flow; + `.github/workflows/publish-commit-bottles.yml` there runs `pr-pull --no-upload` + then `pr-upload` on a `workflow_dispatch`. + +GHCR **storage/consumption** (the install side): + +- `brew pr-upload` publishes each bottle to + `ghcr.io/v2///`, with the OCI + manifest **tagged by `version_rebuild`** (formula version, plus bottle tag and + rebuild), layer media type `application/vnd.oci.image.layer.v1.tar+gzip`. +- At install, Homebrew's GitHubPackages download strategy fetches the OCI + image **index/manifest by version** + (`ghcr.io/v2/...//manifests/`, `Accept: + application/vnd.oci.image.index.v1+json` + bearer token), selects the layer + for the running platform tag, then GETs that layer **blob** + (`.../blobs/sha256:`). The `bottle do` `root_url` selects this strategy. + +### Kandelo custom publish path (as-built) + +Stage map, with the builtin each stage does or does not use: + +| Stage | Custom implementation | Builtin used? | +|---|---|---| +| Plan `(formula, arch)` matrix, skip by `cache_key_sha` | `homebrew-plan-matrix.sh` | No builtin - Kandelo cache-key concept | +| Set Wasm bottle tag | `HOMEBREW_KANDELO_BOTTLE_TAG` + patch, temp Homebrew worktree | Patches Homebrew's own parser | +| Build + test + bottle | `homebrew-bottle-build.sh` runs `brew install --build-bottle`, `brew test`, `brew bottle --json --merge --write` | **Yes - builtin primitives** | +| Upload bytes to GHCR | `homebrew-ghcr-upload.sh` (`oras push`, blob URL) | No - reimplements `pr-upload` upload, **non-native layout** | +| Generate sidecars | `homebrew-generate-sidecars-from-env.sh` + `xtask homebrew-sidecars` | No builtin equivalent | +| Validate sidecars | `xtask homebrew-validate` | No builtin equivalent | +| VFS image + browser smoke + gallery | `build-homebrew-vfs-image.ts`, Playwright, `homebrew-create-browser-gallery.sh` | No builtin equivalent | +| Commit DSL+sidecars to tap; last-green on failure | `homebrew-publish-sidecars.sh` (state lock, ff-only, push to `main`) | No - `pr-pull` is PR-merge shaped | +| Release assets `bottles-abi-v` | `gh release upload` in the workflow | No builtin equivalent | +| Orchestrate everything | `reusable-homebrew-bottle-publish.yml` on push/dispatch | No - replaces `test-bot` CI | +| Install-time consumption | `homebrew-vfs-fetch.ts` GETs the sidecar blob `url` | No - not `brew install` | + +Two properties of the custom path are decisive for the comparison: + +1. **Publish model is push-to-`main`, not PR-merge.** The workflow triggers on + `push`/`workflow_dispatch`, computes a matrix, and each matrix entry commits + directly to the tap under a state lock + (`homebrew-publish-sidecars.sh:140-159`, `:104-120`). There is no PR to pull. + +2. **Consumption is the VFS fetcher, not `brew install`.** The current supported + consumer is `planHomebrewVfs()`/`buildHomebrewVfs()` fed by + `homebrew-vfs-fetch.ts`, which fetches the sidecar's bottle `url` (a GHCR + **blob** URL) directly with a `WWW-Authenticate` bearer challenge + (`homebrew-vfs-fetch.ts:17-34`). Guest `brew install` is explicitly deferred + (`docs/homebrew-publishing.md:9-10, 309-310`). + +## Users and Operator Workflows + +- **Convoy porter (the immediate user).** Adds `Formula/.rb`, runs the + reusable publish workflow (or its `dry-run`), and expects a bottle + validated + sidecars committed to `Automattic/kandelo-homebrew` and a `bottles-abi-v` + release updated. Cares about: matrix scale to ~73 packages, cache-key skip so + unchanged packages don't rebuild, per-package pass/fail/skip reporting, and + last-green preservation when one package fails. +- **Tap maintainer / release owner (`@brandon`).** Approves what publishes, + wants an auditable trail (provenance reports), controllable rollback that does + not delete GHCR blobs, and a story for ABI bumps. Cares about how much bespoke + CI they must understand and keep alive. +- **Homebrew-literate reviewer.** Reads `Formula/*.rb` and expects standard + Homebrew DSL and a recognizable tap. Cares that the tap "looks like a tap" and + that bottles live in GHCR the normal way (this is the constituency the + `pr-upload`-layout convergence serves). +- **Guest `brew install` user (future).** Runs `brew install + automattic/kandelo-homebrew/` on a Kandelo-capable host. Does not exist + yet; is the reason the upload-layout question matters. +- **Kandelo host/browser runtime (today's consumer).** Consumes sidecars + blob + bytes via the VFS builder. Indifferent to OCI manifest layout - it uses the + blob URL. + +## Architecture and Data/Control Flow + +Custom path (today): + +``` +push/dispatch + → plan matrix (cache_key_sha skip) [homebrew-plan-matrix.sh] + → per (formula,arch): + brew install --build-bottle / test / bottle [homebrew-bottle-build.sh] + oras push bottle → GHCR blob [homebrew-ghcr-upload.sh] + generate + validate sidecars [xtask homebrew-sidecars/validate] + (hello wasm32) build VFS + browser smoke + gallery + state-lock → commit DSL+sidecars → push main [homebrew-publish-sidecars.sh] + gh release upload → bottles-abi-v + → consumer: VFS fetcher GETs sidecar blob url [homebrew-vfs-fetch.ts] +``` + +Homebrew builtin path (as `homebrew-core` runs it): + +``` +open PR editing Formula/.rb + → test-bot (per runner-platform): + brew install --build-bottle / test / bottle + upload bottle files + *.bottle.json as workflow artifacts + → maintainer: brew pr-pull + download PR artifacts + brew pr-upload → GHCR OCI (manifest tagged by version) + commit bottle do DSL, push/merge to default branch + → consumer: brew install → GitHubPackages strategy + GET manifests/ → select platform layer → GET blob +``` + +The two pipelines share the middle (the bottle primitives) and diverge at both +ends: Kandelo's front end is a version-agnostic **matrix over a synthetic tag** +instead of a **per-runner-platform** build, and Kandelo's back end is a +**state-locked push to `main` with sidecars** instead of a **PR pull/merge**. +The GHCR step in the middle is where the layouts silently differ. + +## Component-by-Component Comparison and Recommendation + +| Concern | Custom today | Homebrew builtin | Recommendation | Why | +|---|---|---|---|---| +| Compile/test/bottle | builtin primitives via wrapper | same primitives (`test-bot` wraps) | **Keep** | Already builtin; wrapper adds only env + tag + retry | +| Wasm bottle tag | patch + forced tag | none | **Keep** | Needed under any automation; Homebrew has no Wasm system | +| GHCR **upload layout** | `oras push`, blob-only, tag `bottles-abi-v--` | `pr-upload`, native OCI, tag by version | **Reconcile → `pr-upload --upload-only`** | Only builtin that fits; unblocks guest install; low risk | +| CI orchestration | reusable workflow + matrix + cache-key skip | `test-bot` (PR, runner-tag) | **Keep** | `test-bot` assumes runner==target and PR trigger | +| Tap commit / merge | push to `main`, state lock, last-green | `pr-pull` (PR merge) | **Keep** | No PR to pull; sidecars/last-green have no home in `pr-pull` | +| Sidecars / VFS / gallery / provenance | custom | none | **Keep** | No builtin exists; core Kandelo value | +| Release assets | `gh release upload` | none | **Keep** | No builtin exists | +| Install-time download | VFS fetcher (blob url) | GitHubPackages OCI strategy | **Keep for VFS; add native for guest** | VFS is blob-based; guest needs native layout | + +Net: one cell changes ("Reconcile"), everything else is "Keep". The custom path +is mostly *not* reinvention - it is the connective tissue Homebrew does not +provide for a cross-compiled, push-published, sidecar-bearing target. + +## The GHCR Layout Divergence (key finding) + +This is the substantive technical discovery of the research and the basis for +the one recommended change. + +**What Kandelo uploads today** (`homebrew-ghcr-upload.sh:89-113`): an image at +`ghcr.io/v2///` tagged +`${RELEASE_TAG}-${ARCH}-${SHA_PREFIX}` (e.g. +`bottles-abi-v15-wasm32-`), one bottle layer with media type +`application/vnd.homebrew.bottle.layer.v1+gzip`, and a sidecar `url` pointing at +the **blob**: `ghcr.io/v2////blobs/sha256:`. +Meanwhile `homebrew-bottle-build.sh:170` calls `brew bottle --root-url +https://ghcr.io/v2//`, so the formula's `bottle do` block gets a +**GHCR** `root_url`. + +**What a stock `brew install` expects** from that GHCR `root_url`: the +GitHubPackages download strategy resolves the bottle by fetching the OCI +**index/manifest tagged by the formula version** +(`...//manifests/`), not by `bottles-abi-v--`. +Homebrew's own upload path tags manifests `version_rebuild` +(`github_packages.rb`). Kandelo publishes neither a version-tagged index nor +version-tagged per-platform manifests, and uses a different layer media type. + +**Consequence:** the current upload is fully sufficient for Kandelo's VFS +fetcher (which bypasses manifest resolution and GETs the blob URL from the +sidecar), but a stock guest `brew install` against the same `root_url` **cannot +resolve the bottle**. This is not a defect in today's scoped deliverable (guest +install is explicitly out of scope), but it is a **latent blocker** that must be +recorded so nobody assumes `brew tap && brew install` works, and so the +`homebrew-all` fan-out does not bake a non-native layout into 73 packages. + +**Unblock options (either satisfies the goal):** + +- **(A) Adopt `brew pr-upload --upload-only --root-url=`** for the upload + step, replacing `oras push`. `pr-upload` runs standalone on the locally-built + bottle files + `*.bottle.json` that `homebrew-bottle-build.sh` already + produces, emits the native version-tagged OCI layout, and runs `brew audit`. + Kandelo still reads the resulting bottle URL/sha back into the sidecar for the + VFS fetcher. Pro: reuses upstream, gets audit + correct layout for free, is + the exact tool Homebrew maintains for this. Con: adds a dependency on + `pr-upload`'s CLI surface and its assumptions about the working directory and + DSL rewrite; needs the tag patch present so it round-trips the Wasm tag. +- **(B) Extend `homebrew-ghcr-upload.sh`** to additionally push a version-tagged + OCI image index (and matching layer media type) so both the VFS fetcher + (blob) and guest `brew install` (manifest) resolve. Pro: no new CLI + dependency, keeps full control of the OCI graph, smallest change to the + existing shape. Con: Kandelo now owns an OCI-index emitter and must track + Homebrew's manifest/tag conventions over time - exactly the maintenance the + builtin exists to absorb. + +**Recommendation: prefer (A)** for correctness-by-reuse, keep (B) as the +fallback if `pr-upload`'s DSL-rewrite or working-directory assumptions fight the +Kandelo layout in practice. **Defer either until guest `brew install` is a +committed goal** - doing it now would add risk to the `homebrew-all` fan-out for +a consumer that does not yet exist. Until then, record the divergence in +`docs/homebrew-publishing.md` and keep the VFS fetcher as the only supported +consumer. + +## Why not adopt `brew test-bot` / `brew pr-pull` (detail) + +- **Runner-platform assumption.** `test-bot` builds the bottle for the platform + of the runner it executes on and tags it accordingly. Kandelo cross-compiles + `wasm32`/`wasm64` for a synthetic `:kandelo` system on an `x86_64` Linux + runner. Producing a `wasm32_kandelo` bottle requires the tag patch plus a + forced tag (`HOMEBREW_KANDELO_BOTTLE_TAG`), overriding exactly the detection + `test-bot` is built around. `brew test` under `test-bot` would also expect to + execute a host-native artifact; Kandelo's `test do` instead runs the Wasm + through the kernel/Node harness (`Formula/hello.rb` `test do`). The formula + already handles that, but `test-bot`'s lifecycle around it does not. +- **PR-merge vs push-to-`main`.** `pr-pull` pulls bottle artifacts from a + reviewed PR and merges it. Kandelo publishes from `main` on push/dispatch with + a matrix, no PR. Reshaping publishing to be PR-per-bottle to satisfy `pr-pull` + is a larger change than keeping the current orchestrator, and it fights the + convoy's push-driven wave model. +- **Sidecar/cache-key blindness.** `pr-pull`/`pr-upload` know only the `bottle + do` block. They do not generate or validate `Kandelo/` sidecars, build VFS + images, gate browser support, honor `cache_key_sha` skip, write + `bottles-abi-v` release assets, or implement last-green fallback. Those are + the majority of the custom back end and would still have to run alongside, + eroding the "reuse the builtin" benefit. +- **Maintenance math.** The custom orchestration we would retire (`test-bot`'s + slice) is a few hundred lines of workflow + `plan-matrix.sh`. The + Kandelo-specific work we would still own is larger and unavoidable. The trade + removes little and adds a model mismatch. + +## Alternatives Considered + +1. **Status quo, no change (keep `oras push` blob-only).** Correct for the VFS + consumer; leaves guest `brew install` silently broken. Acceptable *only* if + guest install is permanently out of scope - which the docs do not commit to + ("Current Gaps" lists it as future). Rejected as the long-term answer; kept + as the interim state with the divergence documented. +2. **Full adoption of `test-bot` + `pr-pull` + `pr-upload`.** Rejected: model + mismatch (runner-tag, PR-merge, sidecar-blind) costs more than it saves, per + the detail above. +3. **Recommended hybrid: keep custom orchestration/commit/sidecars, converge the + upload step on `pr-upload` layout when guest install is pursued.** Selected: + preserves every Kandelo-specific behavior, reuses the builtin exactly where + the builtin is authoritative (GHCR OCI layout), and is stageable behind a + real trigger. +4. **Switch bottle hosting to GitHub Releases `root_url` instead of GHCR.** A + plain-HTTPS `root_url` makes Homebrew GET `/` + directly, sidestepping OCI entirely - the simplest guest-install-compatible + option. Rejected as the primary path because the prior design deliberately + chose GHCR as the canonical bottle host to match Homebrew norms + (2026-06-25 design "Position"), but retained as an explicit owner-approved + fallback if GHCR OCI upload proves too costly at 73-package scale. +5. **`brew tap-new --github-packages` regenerate the tap from scratch.** + Rejected: it would discard the sidecar layout, matrix, and publish model + already reviewed and merged; it is a scaffolding convenience, not a migration + path for an existing bespoke tap. + +## Risks and Mitigations + +| Risk | Mitigation | +|---|---| +| `homebrew-all` bakes the non-native blob layout into 73 packages, entrenching a guest-install blocker | Record the divergence in `docs/homebrew-publishing.md` now; do not advertise `brew install`; keep the sidecar `url`/sha as the stable contract so an upload-layout swap is URL-only for the VFS fetcher | +| Adopting `pr-upload` later requires reprocessing already-published bottles | The `bottles-abi-v` model already supports republish; a layout migration is a rebuild/repair pass (existing `repair-only` path), not a new mechanism | +| `pr-upload` DSL rewrite conflicts with Kandelo's `bottle --merge` output or forced Wasm tag | Gate adoption behind a spike that runs `pr-upload --upload-only --dry-run` against a `hello` bottle with the tag patch applied; fall back to option (B) if it fights the layout | +| Media-type mismatch (`vnd.homebrew.bottle.layer.v1+gzip` vs `vnd.oci.image.layer.v1.tar+gzip`) blocks native resolve even if tags are fixed | Verify the exact layer media type current Homebrew expects during the spike; align the uploader (or use `pr-upload`, which sets it correctly) | +| Decision is read as "the custom path is wrong" and triggers churn | This document's position is "the custom path is mostly right"; only the upload layout converges, and only on a trigger | +| Keeping bespoke orchestration is long-term maintenance load as Homebrew evolves | Accept consciously: the bespoke parts are Kandelo-specific with no builtin; track Homebrew `bottle`/`pr-upload` CLI changes since we depend on those primitives regardless | + +## Implementation Sequence (only if/when guest `brew install` is pursued) + +Staged so nothing lands speculatively: + +1. **Trigger.** An owner decision to support guest `brew install` (or a + Homebrew-literate consumer that needs native tap install). Until then, only + step 2 lands. +2. **Now (docs-only).** Add a "GHCR upload layout" note to + `docs/homebrew-publishing.md`: today's upload is blob-addressable for the VFS + fetcher and is **not** natively `brew install`-resolvable; guest install is + not supported. (Prevents the fan-out from implying otherwise.) +3. **Spike.** Run `brew pr-upload --upload-only --dry-run --root-url=` + against a locally-built `hello` bottle inside the dev-shell with the tag + patch applied. Confirm: native version-tagged OCI layout, layer media type, + Wasm-tag round-trip, and that the emitted `bottle do` matches Kandelo's + `brew bottle --merge` output. Record pass/fail. +4. **Reconcile upload.** If the spike passes, replace the `oras push` step in + `reusable-homebrew-bottle-publish.yml` with `pr-upload --upload-only`; keep + reading the resulting URL/sha into the sidecar so the VFS fetcher and the + `homebrew-validate` cross-checks are unchanged. If it fails, implement option + (B) in `homebrew-ghcr-upload.sh` (version-tagged OCI index + correct media + type) instead. +5. **Guest-install validation.** Add a Node smoke that does a real `brew + install automattic/kandelo-homebrew/hello` (guest side) against the native + layout, plus the negative ABI-mismatch case - separate from the existing VFS + smoke. +6. **Fan-out.** Only after `hello` validates natively, roll the upload change + through the `homebrew-all` waves; republish existing bottles via the + `repair-only` path so all packages share the native layout. + +## Test and Documentation Plan + +Because the recommended near-term change is docs-only, near-term validation is: + +- `git diff --check` and an ASCII scan of this plan and the + `docs/homebrew-publishing.md` note. +- No code, ABI, package-archive, Node, or browser suites are triggered by a + docs-only change; do not claim otherwise. + +If/when the upload reconciliation lands (step 4+), it is a host/packaging and CI +change and must run the relevant gates, not just unit tests: + +- `bash scripts/dev-shell.sh bash -c 'cargo test -p xtask homebrew'` for any + sidecar/validator touch. +- `cd host && npx vitest run` for VFS fetcher/builder touches (Node + browser + peers). +- The existing Node smoke (`packages/registry/hello/test/homebrew-node-smoke.ts`) + plus a new guest-`brew install` smoke. +- Browser smoke via `./run.sh browser` + the Playwright + `apps/browser-demos/test/kandelo-homebrew.spec.ts` if gallery/VFS output + changes. +- Publish/skip/fail outcome lists per the convoy's test-artifact rules. + +Documentation impact: + +- **Now:** `docs/homebrew-publishing.md` gains the GHCR-layout note (step 2). +- **On reconcile:** update `docs/homebrew-publishing.md` "Trusted Publish Flow" + and "Artifact Model" to describe native OCI upload; update + `docs/binary-releases.md`/`docs/package-management.md` only if the + bottle-vs-package-archive storage story changes; add guest `brew install` + instructions to `README.md` **only** after step 5 passes. + +## Open Questions + +1. Is guest `brew install` a committed goal for `homebrew-all`, or is the VFS + builder the intended terminal consumer? The answer sets whether steps 3-6 + ever run. (Owner decision - `@brandon`.) +2. If guest install is pursued, is GHCR-native OCI required, or is a GitHub + Releases `root_url` (alternative 4) acceptable as the simpler + guest-installable host? This trades Homebrew-norm alignment for + implementation simplicity at 73-package scale. +3. Does `brew pr-upload` cleanly round-trip the `wasm32_kandelo`/`wasm64_kandelo` + tag and `:kandelo` system with the patch applied, or does its DSL/audit path + assume a real platform? (Resolved by the step-3 spike.) +4. What is the exact layer media type current Homebrew's download strategy + requires, and does the checked-in `vnd.homebrew.bottle.layer.v1+gzip` need to + change to `vnd.oci.image.layer.v1.tar+gzip`? (Resolved by the spike.) +5. At 73 packages, is per-package `oras`/`pr-upload` GHCR upload throughput a + bottleneck that argues for batching or `skopeo copy --all`, independent of + the layout question? +6. Should the tap ever also carry native per-runner-platform bottles (so + Homebrew-literate reviewers can `brew test-bot` the tap), or is the + synthetic Wasm tag the only bottle Kandelo will ever ship? + +## Sources + +Kandelo (this checkout, `main` @ `1ab41fe2a`): + +- `docs/homebrew-publishing.md` +- `docs/plans/2026-06-25-homebrew-ci-github-packages-bottle-publishing-design.md` +- `docs/plans/2026-06-27-homebrew-tap-layout-metadata-schema-design.md` +- `docs/plans/2026-06-28-homebrew-vfs-builder-pour-link-design.md` +- `scripts/homebrew-bottle-build.sh`, `scripts/homebrew-ghcr-upload.sh`, + `scripts/homebrew-publish-sidecars.sh`, `scripts/homebrew-plan-matrix.sh`, + `scripts/homebrew-generate-sidecars-from-env.sh` +- `.github/workflows/reusable-homebrew-bottle-publish.yml` +- `homebrew/patches/0001-add-kandelo-wasm-bottle-tags.patch` +- `homebrew/kandelo-homebrew/Formula/hello.rb` +- `host/src/homebrew-vfs-fetch.ts` +- `docs/binary-releases.md`, `docs/package-management.md` + +Homebrew (upstream): + +- Homebrew Bottles: https://docs.brew.sh/Bottles +- Homebrew tap maintenance (`brew tap-new --github-packages`, `brew pr-pull + --head-sha`): https://docs.brew.sh/How-to-Create-and-Maintain-a-Tap +- BrewTestBot: https://docs.brew.sh/BrewTestBot +- `pr-upload` implementation (standalone local-dir upload, flags, audit): + https://github.com/Homebrew/brew/blob/master/Library/Homebrew/dev-cmd/pr-upload.rb +- `github_packages.rb` (OCI tag `version_rebuild`, repo path, media types): + https://github.com/Homebrew/brew/blob/master/Library/Homebrew/github_packages.rb +- `homebrew-core` publish workflow (`pr-pull --no-upload` + `pr-upload`, + `HOMEBREW_GITHUB_PACKAGES_*`): + https://github.com/Homebrew/homebrew-core/blob/master/.github/workflows/publish-commit-bottles.yml From 35279a4f0a6fe12f73ec7700c80c9cb640482dfe Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Sun, 5 Jul 2026 10:20:12 -0400 Subject: [PATCH 2/5] docs(homebrew): in-progress bottle-publishing design checkpoint Durable checkpoint of the ongoing Homebrew bottle-publishing design discussion so state survives a session reset. Captures goal/scope, each facet's decision/status, locked decisions (parity-first GHCR-native publish via brew bottle + skopeo/pr-upload, PR->merge->release split, ABI-in-platform-tag, :any, sidecar decomposition, pour via brew-in-Kandelo, lazy option 3), open questions (Ruby GC-root-visibility / wasm-local-root-spill + kd-5mb libyaml timing, sequencing), the in-progress recommendation, next steps, and pointers (PR #823, kd-1i0u, kd-yuef, kd-1mr, kd-p3hr, kd-v3fs). Living document; supersedes several conclusions of the 2026-07-01 research doc, which still needs correcting. Co-Authored-By: Claude Opus 4.8 --- ...rew-bottle-publishing-design-checkpoint.md | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 docs/plans/2026-07-05-homebrew-bottle-publishing-design-checkpoint.md diff --git a/docs/plans/2026-07-05-homebrew-bottle-publishing-design-checkpoint.md b/docs/plans/2026-07-05-homebrew-bottle-publishing-design-checkpoint.md new file mode 100644 index 000000000..c221e3a2c --- /dev/null +++ b/docs/plans/2026-07-05-homebrew-bottle-publishing-design-checkpoint.md @@ -0,0 +1,119 @@ +# Homebrew Bottle-Publishing Design — In-Progress Checkpoint + +- Date: 2026-07-05 +- Status: **IN-PROGRESS design discussion** (Brandon @brandon ↔ designer). Living document — update as the discussion continues so it always reflects latest state. +- Purpose: durable capture so little is lost if the session resets. This **supersedes several conclusions** of the prior research doc (`docs/plans/2026-07-01-homebrew-builtin-vs-custom-bottle-publishing-research.md`, PR #823); that doc still needs correcting (see §6). +- Mode: deliberate, facet-by-facet, discuss-first-then-implement. No implementation started yet. + +## 1. Goal and scope + +Design how Kandelo **publishes and consumes Homebrew-style packages (bottles)**. + +**Governing principle (locked, Brandon): fidelity to Homebrew.** Kandelo packages are first-class Homebrew citizens — hosted where Homebrew hosts (GHCR/OCI), published with Homebrew's own tooling, and resolvable by a stock guest `brew` identically to `homebrew/core`. "Use the real thing, the real way; Kandelo's sandbox is what makes running real brew safe." + +Scope: bottle format/tags/cellar, GHCR hosting, the publish permission model, the VFS build/pour model (build-time + runtime), lazy on-demand materialization, and the near-term unblock of **kd-yuef** (VFS-critical language sidecars: cpython/perl/erlang). + +Non-goals: not reviving the custom `oras` blob path; not a TS/Rust reimplementation of `brew` pour; not changing the fork/ABI machinery. + +## 2. Facets and status + +| # | Facet | Status | Decision | +|---|-------|--------|----------| +| 1 | wasm32 tag/cellar fit | ✅ done | Tags collapse arch×OS → `{wasm32,wasm64}_kandelo`. Corrections folded: two consumers both honor pour semantics; trust rationale for not running Formula Ruby in the raw host/browser; **`:any`** relocatable (not `:any_skip_relocation`). | +| 2 | kd-yuef blocked operationally, not architecturally | ✅ done | Block = permissions/plumbing (privileged manual publish + uncommitted formulae + `TAP_SOURCE` bug), **not** custom-vs-builtin tooling. | +| 3 | Build/publish permission split | ✅ done | = bring bottle publish into Kandelo's **existing PR→merge→release model**. The current `workflow_dispatch`-only publish is the **outlier = kd-yuef root cause**. | +| a | GHCR vs GitHub Releases | ✅ resolved | **GHCR-native, exact Homebrew parity.** GH Releases = legacy *present*, retired for bottles. | +| b | Split-now vs hand-crank | ✅ resolved | **b2′: seed-then-automate** — publish bottles now (no-regret), one-time privileged seed, automate the split next. | +| c | wasm64 asymmetry / `:any` cost | ✅ resolved | Kernel is wasm32 but **runs wasm64 binaries as processes** → `wasm64_kandelo` is a co-equal live tag. Both `:any`. `:any` cost ≈ 0 today (fixed prefix ⇒ no relocation). | +| d | Open thread | ✅ empty | Nothing further from Brandon. | +| — | Pour model | ✅ corrected | **Real Homebrew inside Kandelo does the pour.** Link-manifest replay is *replaced*, not preserved. Gated on Ruby. (See §3.) | +| — | Lazy files | ✅ decided | Materialize VFS files on demand **from the published bottle**. **Option 3** (whole tar.gz archive lazy fetch) now; **option 2** (derived per-file/zip rep) documented as the restore path. (See §3.) | +| R | Recommendation | 🟡 in progress | Synthesized below (§5); final pending the sequencing decision. | +| D | Doc-correction | ⬜ pending | Fix PR #823 doc + kd-1i0u note (§6). | + +## 3. Decisions locked + +**Hosting & repo boundary** +- Bottles/packages → **GHCR/OCI**, exact Homebrew parity, guest-`brew`-resolvable like `homebrew/core`. +- Packages + live tap state (Formulae, `bottle do` blocks, provenance) + **GHCR images** → **`Automattic/kandelo-homebrew`** (the tap). The main repo **`Automattic/kandelo`** owns only tooling (schemas, validators, workflow generator, tap fixture/template). +- GHCR namespace mirrors the tap (≈ `ghcr.io/automattic/kandelo-homebrew/`; exact path = owed source-verification). +- **No binaries in git.** Bottle *bytes* → GHCR; only bottle *blocks* (text DSL) + Formulae → the tap git repo. This is exactly what Homebrew does. + +**Tooling** +- Produce bytes with `brew … --build-bottle` + `brew bottle`. +- Publish to GHCR with **`skopeo`** via `brew pr-upload` / `pr-pull` (Homebrew's own uploader — `Homebrew/brew` `github_packages.rb`). `skopeo` is a **publish-side** dep only (install side = plain OCI HTTP). +- `pr-upload --upload-only` = privileged **maintenance/seed** hatch (first-bottle seeding, bulk ABI re-bottle). PR→merge→`pr-pull` = steady-state contributor path. + +**Permission split** +- = Kandelo's existing **PR→merge→release** model, run in the **tap repo's** CI: unprivileged build/validate on the PR; privileged `skopeo` publish + tap commit on maintainer merge. +- `packages:write` token now **provisioned by Brandon**. + +**Tag / ABI model** +- Synthetic `:kandelo` Homebrew system; tags `{wasm32,wasm64}_kandelo`. +- **Kernel ABI generation is encoded in the platform tag** (kernel ABI ≈ macOS version: both say "which platform generation is this binary compatible with"). Multiple ABIs coexist as distinct tags. Not `rebuild`, not per-ABI taps. +- **`:any`** relocatable for both arches (normalized; the wasm64 fixture's absolute cellar was an inconsistency). Cheap because everything targets the single fixed prefix. + +**Bottle self-description (sidecar decomposition)** +- Extrinsic identity (`url`/`sha256`/`cellar`/`rebuild`) → brew's `bottle do` (stop duplicating in a sidecar). +- Intrinsic pour data (`link_manifest`/`fork_instrumentation`/`cache_key_sha`) → an in-keg `.kandelo/` receipt (ships inside the bottle; keeps git text-light). +- Status (`browser_compatible`) → a **publish gate** in the test phase, not a stored field. + +**Pour model (corrected)** +- **One implementation: real Homebrew running inside Kandelo** (the sandbox is the trust boundary, so this is safe even in the browser). + - **Build-time VFS:** host runs Kandelo → brew-in-Kandelo installs the packages → **save the resulting VFS image** (`saveImage()`), preserving heavy files as lazy references. + - **Runtime:** guest `brew` installs on demand, in Kandelo. +- The committed host-side Rust **link-manifest replay** (`tools/xtask/src/homebrew_vfs_build.rs`) is the mechanism being **replaced**, not preserved. It exists today and works without brew; the target routes through brew-in-Kandelo. +- **Gated on Ruby** (see §4). + +**Lazy materialization** +- Lazy files/archives are a first-class, already-built VFS feature (`docs/architecture.md` §Lazy Files/§VFS Images; memfs; mkrootfs manifest; `docs/package-management.md` §Rootfs lazy binary manifests). VFS image format carries a lazy-entries section (`{ino,path,url,size}`); `saveImage()` preserves URL refs or `materializeAll`. +- Lazy files materialize **from the published bottle**. **Decision: option 3** — extend the lazy-*archive* path (zip-only today) to fetch+extract a **whole tar.gz bottle** on first access to any member, for Homebrew-sourced files (coexists with, does not remove, the existing per-file lazy path for other sources). +- **Document clearly:** option 2 (a per-file/zip representation *derived* from the canonical tar.gz bottle — the bottle stays tar.gz) is the sanctioned way to **restore per-file lookup**. Reconsideration trigger: whole-bottle fetch measured too heavy for the browser on large kegs. (Homebrew bottles are canonically tar.gz — a gzip stream with no random access; zip is not a supported bottle format. tar.gz ⇒ whole-archive; zip ⇒ per-file.) + +**Rollout** +- **b2′**: publish the cpython/perl/erlang bottles now (needed under both the current consumer and the target lazy source — no-regret); seed via one-time privileged `pr-upload --upload-only`; automate the split next. Reject b1 (block P1 on the automation lift) and old-`oras` hand-crank (non-parity throwaway). + +## 4. Open questions in flight + +1. **Sequencing (active topic).** Does kd-yuef unblock **now via the transitional link-manifest path** (greens the composite wave immediately, but that mechanism effort is throwaway — the bottles are not), or **wait for brew-in-Kandelo** (ties a P1 to the Ruby long-pole)? Deciding factor = **Ruby timeline**. +2. **Ruby completeness (the gate for guest-brew / brew-in-Kandelo).** Ruby 4.0.5 builds ABI-15/fork-instrumented and runs simple scripts, but `require 'rubygems'` and `require 'uri'` **trap under GC** (needs the **designed-but-unlanded `wasm-local-root-spill`** pass — `docs/plans/2026-06-19-ruby-wasm-gc-root-visibility-design.md`), and **Psych/YAML is broken (kd-5mb)**. brew exercises exactly these. **Timeline unknown — the key input to the sequencing decision.** +3. **Lazy granularity.** Option 3 now; option 2 (per-file/zip derived rep) if whole-bottle fetch too heavy — needs measurement. +4. **Link-manifest generator** (design Open Q #1 in `docs/plans/2026-06-18-homebrew-vfs-builder-design.md`): **unbuilt** (only synthetic test fixtures). Under the target it becomes brew-in-Kandelo's output; near-term for kd-yuef it may need a stopgap generator. +5. **Owed verification against `Homebrew/brew` source:** `github_packages.rb`/`skopeo`; `pr-upload`/`pr-pull` standalone + synthetic-tag handling; GHCR path derivation for the tap; whether `brew bottle` has any non-tar.gz format flexibility (settles option 2's "zip"). +6. **Transitional consumer:** keep the link-manifest replay path alive until brew-in-Kandelo lands, or not? + +## 5. In-progress Recommendation + +Parity-first, native-only publish + brew-in-Kandelo consume: + +1. **Publish** cpython/perl/erlang bottles to GHCR under `Automattic/kandelo-homebrew` via `brew bottle` + `skopeo`/`pr-upload`. No `oras`, single native layout. +2. **Split** the publish along Kandelo's existing PR→merge→release model in the tap repo (unprivileged build on PR; privileged publish on merge). `--upload-only` for seeding/maintenance. +3. **Tags:** `{wasm32,wasm64}_kandelo`, ABI generation in the platform tag, `:any`. +4. **Bottle self-describing:** `bottle do` + in-keg `.kandelo/` receipt; browser smoke as a publish gate. +5. **Pour:** real Homebrew inside Kandelo — build-time install→`saveImage()`, runtime guest install. Replace the link-manifest replay. +6. **Lazy:** option 3 (whole tar.gz archive) now; option 2 documented as the per-file restore path. +7. **Rollout:** b2′ seed-then-automate. + +**Publish track = no-regret (do now). Consume/pour track = gated on Ruby.** Final recommendation pending the §4.1 sequencing decision. + +## 6. Next steps + +1. Resolve **sequencing** (kd-yuef transitional vs wait-for-brew-in-Kandelo) — pending Brandon's read on the Ruby timeline. +2. Consolidate the **Recommendation** facet. +3. **Doc-correction:** fix `docs/plans/2026-07-01-homebrew-builtin-vs-custom-bottle-publishing-research.md` + the kd-1i0u note — guest `brew install` is **in-scope** (not "VFS builder is terminal consumer"); `:any` (not `:any_skip_relocation`); **native GHCR/parity**; supersede "do not adopt `test-bot`/`pr-pull`" (adopt the PR→merge→`pr-pull` split); pour = brew-in-Kandelo; lazy = option 3. +4. **Verification pass** against `Homebrew/brew` source (§4.5). +5. Stand up **implementation beads** with dependency edges: b2′ seed (kd-p3hr formula checkpoint = long-pole), tap-repo PR→merge automation, synthetic-`:kandelo`-tag spike, and the Ruby GC/YAML track linkage (kd-5mb + the `wasm-local-root-spill` work). + +## 7. Pointers + +- **PR #823** (draft, Automattic/kandelo): the prior research doc — being corrected. +- **kd-1i0u** (closed): research bead; `github_pr` = #823; worktree = this one. +- **kd-yuef** (P1, blocked): VFS-critical language sidecars (cpython/perl/erlang). +- **kd-1mr**: umbrella convoy (homebrew-all). +- **kd-p3hr**: worktree holding the uncommitted cpython/perl/erlang Formulae (checkpoint long-pole for b2′). +- **kd-v3fs**: composite VFS packages (downstream of kd-yuef). +- **kd-5mb**: libyaml/Psych fix (Ruby gate). +- Ruby: `packages/registry/ruby/` (4.0.5, ABI-15, fork-instrumented). GC design: `docs/plans/2026-06-19-ruby-wasm-gc-root-visibility-design.md`. +- Code: `tools/xtask/src/homebrew_vfs_build.rs` (current link-manifest builder), `tools/xtask/src/homebrew_vfs_plan.rs` (`LinkManifest`; `DEFAULT_PREFIX="/home/linuxbrew/.linuxbrew"`), `tools/mkrootfs/` (VFS serialization), memfs lazy files in `host/src/*`. +- Docs: `docs/architecture.md` §Lazy Files/§VFS Images; `docs/package-management.md` §Rootfs lazy binary manifests; `docs/plans/2026-06-18-homebrew-vfs-builder-design.md` (Open Q #1 = link-manifest generator); `docs/plans/2026-06-18-homebrew-runtime-bringup-design.md`; `docs/plans/2026-06-18-homebrew-portable-ruby-strategy.md`. +- Guest-brew probe evidence (in-progress experiment, not shipped): `target/kd-sqw-homebrew/` (booted upstream Homebrew image); live work in sibling worktrees outside the main checkout. From 4f175fc796c4359f0d3bf0472e75348bf540f39f Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Sun, 5 Jul 2026 16:14:48 -0400 Subject: [PATCH 3/5] =?UTF-8?q?docs(homebrew):=20checkpoint=20update=20?= =?UTF-8?q?=E2=80=94=20sequencing=20resolved=20+=20Ruby=20status=20correct?= =?UTF-8?q?ed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sequencing RESOLVED (Brandon): wait for brew-in-Kandelo; abandon the transitional link-manifest unblock for kd-yuef. - Ruby status CORRECTED from bead ground truth: GC root-visibility (wasm-local-root-spill) is MERGED (PR #718, 2026-06-24); psych/YAML works at runtime (PR #814 open, code-done); brew-config Ruby traps closed. Ruby is not a distant long-pole; the remaining critical path is the brew-in-Kandelo integration itself. Co-Authored-By: Claude Opus 4.8 --- ...26-07-05-homebrew-bottle-publishing-design-checkpoint.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/plans/2026-07-05-homebrew-bottle-publishing-design-checkpoint.md b/docs/plans/2026-07-05-homebrew-bottle-publishing-design-checkpoint.md index c221e3a2c..4414a94a8 100644 --- a/docs/plans/2026-07-05-homebrew-bottle-publishing-design-checkpoint.md +++ b/docs/plans/2026-07-05-homebrew-bottle-publishing-design-checkpoint.md @@ -75,8 +75,8 @@ Non-goals: not reviving the custom `oras` blob path; not a TS/Rust reimplementat ## 4. Open questions in flight -1. **Sequencing (active topic).** Does kd-yuef unblock **now via the transitional link-manifest path** (greens the composite wave immediately, but that mechanism effort is throwaway — the bottles are not), or **wait for brew-in-Kandelo** (ties a P1 to the Ruby long-pole)? Deciding factor = **Ruby timeline**. -2. **Ruby completeness (the gate for guest-brew / brew-in-Kandelo).** Ruby 4.0.5 builds ABI-15/fork-instrumented and runs simple scripts, but `require 'rubygems'` and `require 'uri'` **trap under GC** (needs the **designed-but-unlanded `wasm-local-root-spill`** pass — `docs/plans/2026-06-19-ruby-wasm-gc-root-visibility-design.md`), and **Psych/YAML is broken (kd-5mb)**. brew exercises exactly these. **Timeline unknown — the key input to the sequencing decision.** +1. **Sequencing — RESOLVED (2026-07-05, Brandon): wait for brew-in-Kandelo.** kd-yuef does **not** take the transitional link-manifest unblock; the link-manifest replay path is abandoned, not kept alive. kd-yuef's unblock now depends on brew-in-Kandelo being usable. +2. **Ruby status — CORRECTED (bead ground truth; the earlier committed-checkout snapshot was stale).** The GC root-visibility / `wasm-local-root-spill` work is **DONE and MERGED** — PR #718 merged to main 2026-06-24 (kd-drt.2/kd-drt.9/kd-26r closed); URI/RubyGems GC traps fixed. Psych/YAML **works at runtime** (psych 5.3.1, libyaml 0.2.5, YAML round-trip verified on Node via kd-egn1; psych on main via #774); **PR #814 (open, non-draft, base=main)** adds the psych/YAML `ruby.rb` formula, reconciled with #810, held open only on the kd-yuef bottle/browser gate. brew-config Ruby traps closed (kd-e8g/kd-xmh/kd-9jd/kd-nfr/kd-drt.13/kd-v8m/kd-bsj). **So Ruby is NOT a distant long-pole.** The real remaining critical path is the **brew-in-Kandelo *integration*** (guest `brew install`/doctor/tap end-to-end + driving the VFS build via brew-in-Kandelo) — the in-progress experiment (`target/kd-sqw-homebrew/` boot probe + sibling worktrees), whose exact state needs assessment. A dedicated agent prompt for this was produced (assess-first, then drive). 3. **Lazy granularity.** Option 3 now; option 2 (per-file/zip derived rep) if whole-bottle fetch too heavy — needs measurement. 4. **Link-manifest generator** (design Open Q #1 in `docs/plans/2026-06-18-homebrew-vfs-builder-design.md`): **unbuilt** (only synthetic test fixtures). Under the target it becomes brew-in-Kandelo's output; near-term for kd-yuef it may need a stopgap generator. 5. **Owed verification against `Homebrew/brew` source:** `github_packages.rb`/`skopeo`; `pr-upload`/`pr-pull` standalone + synthetic-tag handling; GHCR path derivation for the tap; whether `brew bottle` has any non-tar.gz format flexibility (settles option 2's "zip"). @@ -107,6 +107,8 @@ Parity-first, native-only publish + brew-in-Kandelo consume: ## 7. Pointers - **PR #823** (draft, Automattic/kandelo): the prior research doc — being corrected. +- **Ruby/brew PRs:** PR #718 (Ruby 4 + wasm-local-root-spill) **MERGED** 2026-06-24; PR #814 (ruby psych/YAML formula) **OPEN/non-draft**, code-done, gated on kd-yuef; PR #810 (language runtime formulas) OPEN, reconciled to not touch Ruby. +- **brew-in-Kandelo:** guest-brew boot probe `target/kd-sqw-homebrew/`; runtime-bringup design `docs/plans/2026-06-18-homebrew-runtime-bringup-design.md`; live work in sibling worktrees. This integration — not the Ruby runtime — is the remaining critical path for the "wait for brew-in-Kandelo" decision. - **kd-1i0u** (closed): research bead; `github_pr` = #823; worktree = this one. - **kd-yuef** (P1, blocked): VFS-critical language sidecars (cpython/perl/erlang). - **kd-1mr**: umbrella convoy (homebrew-all). From ff7cd78a96fc25aefb257c289b166d798bf4faaa Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Sun, 5 Jul 2026 20:51:37 -0400 Subject: [PATCH 4/5] =?UTF-8?q?docs(homebrew):=20checkpoint=20=E2=80=94=20?= =?UTF-8?q?Formulae=20+=20recipes=20migrate=20to=20the=20tap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records Brandon-confirmed decision: Automattic/kandelo-homebrew becomes the sole home + sole edit point for full package definitions (Formula + build recipe). Whole-move package PRs to the tap (recipe+formula co-locate => no split); platform PRs stay in main and shed formula edits. Tap = edit authority; main keeps a synced copy until brew-in-Kandelo retires it (packages/registry is load-bearing for main's rootfs/VFS/CI builds today). First instance: #814. Co-Authored-By: Claude Opus 4.8 --- ...07-05-homebrew-bottle-publishing-design-checkpoint.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/plans/2026-07-05-homebrew-bottle-publishing-design-checkpoint.md b/docs/plans/2026-07-05-homebrew-bottle-publishing-design-checkpoint.md index 4414a94a8..72b9068fb 100644 --- a/docs/plans/2026-07-05-homebrew-bottle-publishing-design-checkpoint.md +++ b/docs/plans/2026-07-05-homebrew-bottle-publishing-design-checkpoint.md @@ -39,6 +39,13 @@ Non-goals: not reviving the custom `oras` blob path; not a TS/Rust reimplementat - GHCR namespace mirrors the tap (≈ `ghcr.io/automattic/kandelo-homebrew/`; exact path = owed source-verification). - **No binaries in git.** Bottle *bytes* → GHCR; only bottle *blocks* (text DSL) + Formulae → the tap git repo. This is exactly what Homebrew does. +**Formulae + recipes → the tap; whole-move package PRs (2026-07-05, Brandon)** +- The tap is the **sole home and sole edit point** for full package definitions: `Formula/.rb` **plus** the build recipe (`packages/registry//` — build script, `package.toml`, `build.toml`, demos). The main-repo `homebrew/kandelo-homebrew/` fixture is **retired**; formulae stop landing there. Parity end-state: the tap is a real Homebrew tap of real from-source formulae; `Automattic/kandelo` is the platform they build on. +- **Mechanics:** fork the recipes into the tap now; **whole-move** package-definition PRs to the tap (recipe+formula co-locate ⇒ no split). The few *platform* PRs that only incidentally touch formulae (framebuffer/browser-smoke #819/#817/#816) stay in main and **shed** their formula edits. +- **Transition discipline (avoids drift):** tap = single edit point; main keeps a **synced/referenced** copy of the recipes (subtree or generated sync, mechanism TBD) so main's rootfs/VFS/CI builds keep working — `packages/registry` is load-bearing for those today. **Retire main's copy only when brew-in-Kandelo lets main consume packages from the tap.** +- **Open design points:** tap layout for recipes relative to `Formula/`; the tap→main SDK/xtask dependency wiring (correct parity shape); the sync mechanism. +- **First instance:** #814 (`ruby.rb` + `packages/registry/ruby/`) whole-moves to the tap (must NOT merge to `Automattic/kandelo`). + **Tooling** - Produce bytes with `brew … --build-bottle` + `brew bottle`. - Publish to GHCR with **`skopeo`** via `brew pr-upload` / `pr-pull` (Homebrew's own uploader — `Homebrew/brew` `github_packages.rb`). `skopeo` is a **publish-side** dep only (install side = plain OCI HTTP). @@ -98,7 +105,7 @@ Parity-first, native-only publish + brew-in-Kandelo consume: ## 6. Next steps -1. Resolve **sequencing** (kd-yuef transitional vs wait-for-brew-in-Kandelo) — pending Brandon's read on the Ruby timeline. +1. **Migrate Formulae + build recipes → the tap** (Brandon-confirmed 2026-07-05): design tap layout + sync mechanism; fork recipes; whole-move package PRs starting with #814; retire the main fixture; coordinate with mayor (convoy-owned PRs). See §3. *(Sequencing is RESOLVED — wait for brew-in-Kandelo; see §4.)* 2. Consolidate the **Recommendation** facet. 3. **Doc-correction:** fix `docs/plans/2026-07-01-homebrew-builtin-vs-custom-bottle-publishing-research.md` + the kd-1i0u note — guest `brew install` is **in-scope** (not "VFS builder is terminal consumer"); `:any` (not `:any_skip_relocation`); **native GHCR/parity**; supersede "do not adopt `test-bot`/`pr-pull`" (adopt the PR→merge→`pr-pull` split); pour = brew-in-Kandelo; lazy = option 3. 4. **Verification pass** against `Homebrew/brew` source (§4.5). From 495250692766badd641ac623a26ba9417ee5059d Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Sun, 5 Jul 2026 21:42:53 -0400 Subject: [PATCH 5/5] docs(homebrew): standalone work record + forward execution plan (handoff) A self-contained handoff so the Homebrew work can be driven directly with Claude, outside Gas City mayor-routing, with per-PR oversight. Captures: locked design decisions (+ today's tap-layout=Homebrew- idiomatic and sync=main-pulls-from-tap), the current state record (repos, near-empty tap, ~50-PR landscape categorized with dispositions, beads, brew-in-Kandelo status, worktrees), open questions, and a phased bead-ready execution plan (Track A tap migration, Track B brew-in-Kandelo critical path, Track C publish pipeline, Track D cleanup) with a paste- ready brew-in-Kandelo assessment prompt. Co-Authored-By: Claude Opus 4.8 --- ...ebrew-handoff-record-and-execution-plan.md | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 docs/plans/2026-07-05-homebrew-handoff-record-and-execution-plan.md diff --git a/docs/plans/2026-07-05-homebrew-handoff-record-and-execution-plan.md b/docs/plans/2026-07-05-homebrew-handoff-record-and-execution-plan.md new file mode 100644 index 000000000..c28ea7adc --- /dev/null +++ b/docs/plans/2026-07-05-homebrew-handoff-record-and-execution-plan.md @@ -0,0 +1,104 @@ +# Homebrew on Kandelo — Work Record & Forward Execution Plan (Handoff) + +- Date: 2026-07-05 (state snapshots are point-in-time; re-verify PR/bead status at execution). +- Purpose: a **standalone handoff** so this work can be driven **directly with Claude**, outside Gas City mayor-routing, with reliable oversight. +- Companion (design rationale, the "why"): `docs/plans/2026-07-05-homebrew-bottle-publishing-design-checkpoint.md`. +- Prior research (to be corrected): `docs/plans/2026-07-01-homebrew-builtin-vs-custom-bottle-publishing-research.md` (PR #823). + +## 0. How to use this / oversight model + +- This doc is the single source of truth for the Homebrew bottle-publishing + tap-migration + brew-in-Kandelo work. +- **Execution model built for oversight:** every work item below is scoped to a **single reviewable PR** with explicit acceptance. Drive them one at a time with Claude; you review each PR before merge. No mayor routing required. +- **Beads (optional):** if you want tracking, create/own beads yourself as a personal checklist — you don't have to route through the mayor. Each §5 work item maps to one bead. +- **Two GitHub repos:** `Automattic/kandelo` (the platform) and `Automattic/kandelo-homebrew` (the tap). Formula/package work targets the **tap**; platform work targets **main**. + +## 1. Goal & governing principle + +Publish and consume Kandelo packages as **first-class Homebrew citizens**: hosted where Homebrew hosts (GHCR/OCI), published with Homebrew's own tooling, resolvable by a stock guest `brew` like `homebrew/core`. **Real Homebrew runs inside Kandelo** (the kernel sandbox is the trust boundary) to install packages at VFS build time and at runtime. End-state: **the tap is a real Homebrew tap of real from-source formulae; `Automattic/kandelo` is the platform they build on.** + +## 2. Locked decisions (rationale in the checkpoint) + +- **Hosting:** GHCR/OCI at exact Homebrew parity; publish with **`skopeo` via `brew pr-upload`/`pr-pull`** (Homebrew's own uploader). GH Releases retired for bottles. `packages:write` token is provisioned. +- **Repo boundary:** the tap `Automattic/kandelo-homebrew` owns **full package definitions** (Formula + build recipe) + `bottle do` + provenance + **GHCR images**. Main owns the platform (kernel, host/VFS, SDK, CI). **No binaries in git** (bytes → GHCR). +- **Permission split:** bottle publish rides Kandelo's existing **PR→merge→release** model, in the **tap's** CI (unprivileged build/validate on PR; privileged `skopeo` publish + tap commit on merge). `pr-upload --upload-only` = privileged seed/maintenance hatch. +- **Tags/ABI:** synthetic `:kandelo` system; `{wasm32,wasm64}_kandelo` tags; **kernel-ABI generation encoded in the platform tag** (ABI ≈ macOS version); `:any` relocatable (cheap — everything targets the single fixed prefix `/home/linuxbrew/.linuxbrew`). +- **Bottle self-describing:** extrinsic identity → `bottle do`; intrinsic pour data (`link_manifest`/`fork_instrumentation`/`cache_key_sha`) → an in-keg `.kandelo/` receipt; browser smoke → a publish **gate**, not a stored field. +- **Pour model:** **real Homebrew inside Kandelo** — build-time (host runs Kandelo → brew installs → `saveImage()`), runtime (guest `brew install`). The committed host-side link-manifest *replay* (`tools/xtask/src/homebrew_vfs_build.rs`) is being **replaced**. +- **Lazy files:** materialize VFS files on demand **from the published bottle**. **Option 3** — extend the lazy-*archive* path to fetch+extract the whole **tar.gz bottle** on first access. **Option 2** (a per-file/zip representation *derived* from the tar.gz bottle) is the documented restore path if whole-bottle fetch is too heavy. +- **Formulae + recipes → tap (whole-move):** fork recipes into the tap; **whole-move** package PRs (recipe+formula co-locate ⇒ no split). Platform PRs that only incidentally touch formulae stay in main and shed those edits. Retire the main `homebrew/kandelo-homebrew/` fixture. +- **Tap layout → Homebrew-idiomatic (2026-07-05):** build logic lives in the formula's **`install` method** (real from-source formula), with Kandelo specifics (SDK activation, wasm cross-compile, fork-instrument) in the **`Kandelo/KandeloFormulaSupport`** mixin — *not* a separate `build-.sh`. Deviate only on genuine Homebrew-idiom friction, and call each deviation out. +- **Sync → main pulls from the tap (2026-07-05):** no maintained subtree/vendored copy. Main **taps and builds from** the tap on demand; `packages/registry` copies retire once brew-in-Kandelo consumption lands. +- **Rollout:** publish bottles is no-regret. **kd-yuef waits for brew-in-Kandelo** (no transitional link-manifest unblock). + +## 3. Current state (the RECORD) — snapshot 2026-07-05 + +### Repos +- **`Automattic/kandelo`** — platform + (today) the `homebrew/kandelo-homebrew/` *fixture* holding most formulae, plus `packages/registry//` build recipes. These are what migrate/retire. +- **`Automattic/kandelo-homebrew`** — the real tap. **Currently near-empty: `Formula/` has only `hello.rb`.** `Kandelo/` has the support infra (schemas: `formula.schema.json`, `link-manifest.schema.json`, `metadata.schema.json`, `provenance.schema.json`; `examples`, `reports`). So the migration is largely **greenfield** on the tap side. + +### brew-in-Kandelo (the critical path) +- **Ruby foundation is MERGED**, not pending: PR **#718** (Ruby 4 + `wasm-local-root-spill` GC root visibility) merged 2026-06-24; URI/RubyGems GC traps fixed; psych/YAML works at runtime (psych 5.3.1, libyaml 0.2.5); brew-config Ruby traps closed (kd-e8g/kd-xmh/kd-9jd/kd-nfr/kd-drt.13/kd-v8m/kd-bsj). +- **Guest brew is an in-progress experiment, not shipped:** a real upstream Homebrew has been staged + booted inside a Kandelo guest (`target/kd-sqw-homebrew/` probe image). Live work has been happening in **sibling worktrees** outside the main checkout. **True end-to-end state of `brew install`/`doctor`/`tap` inside Kandelo is unassessed** — this is the #1 unknown and the gate for everything downstream. + +### Open PR landscape (categorized; ~50 open PRs — re-verify) +- **Ruby:** #718 **MERGED**; **#814** (psych/YAML formula) OPEN → **redirect to tap** as migration instance #1 (do not merge to main). +- **Package waves (formula + recipe → whole-move to tap):** #810 (language runtimes), #808 (JS/DB/service), #815 (ncurses/bash/nethack/curl), #818 (cpython stdlib), #820 (erlang), #821/#822/#827/#829/#833 (perl), #839 (php), #792/#793/#794 (dependency roots / CLI waves), #809 (spidermonkey). +- **Platform/CI PRs (stay in main; shed formula edits):** #819/#817/#816 (framebuffer + browser-smoke harness), #811 (rootfs/shell sidecars), #812 (VFS dir links + dinit), #795 (generic browser smoke). +- **Registry-reshaping drafts (RECONCILE with this plan — overlapping prior work):** #806 (Homebrew package inventory + migration plan), #805 (registry replacement model), #804 (move runtime platform artifacts out of registry bridge), #799–#803, #800–#802 (registry cleanup/classification). +- **SQLite/PHP platform fixes (normal main work, mostly not formula):** #756/#757/#758, #760–#784, #798, etc. +- **Research/docs:** #823 (this design research — to be corrected), #831 (GHCR blob-vs-native OCI doc), #834 (fork-instrument size eval). + +### Key beads +- **kd-1mr** — umbrella convoy (homebrew-all). **kd-1i0u** — this design (closed; PR #823; worktree holds these docs). **kd-yuef** (P1, blocked) — VFS-critical cpython/perl/erlang sidecars; **now waits for brew-in-Kandelo**. **kd-p3hr** — worktree with the uncommitted/authored language formulae. **kd-v3fs** — composite VFS packages (downstream of kd-yuef). **kd-egn1** (P2, open) — ruby psych/YAML (PR #814); **kd-9oou/kd-dsft** (P3) — ruby CI/build follow-ups. + +### Worktrees +- Design/docs: `/Users/brandon/src/kandelo-gascity/worktrees/kandelo/kd-1i0u-bottle-publishing-research` (branch `gascity/kd-1mr/kd-1i0u-bottle-publishing-research`) — holds this doc + the checkpoint. +- Live guest-brew work: **sibling worktrees** (locate before assuming state). + +## 4. Open questions / risks + +1. **brew-in-Kandelo end-to-end state is unassessed** — the gate for kd-yuef and for main-consumes-from-tap. Resolve first (Phase 2). +2. **Reconcile with in-flight registry-reshaping PRs (#804/#805/#806, #799–#803).** They already touch the registry→homebrew boundary; the tap-migration must absorb or supersede them, not collide. +3. **Homebrew-idiom friction points** (call out as found): wasm cross-compile, fork-instrument, SDK activation, the `:kandelo` synthetic tag, ABI-in-tag — where these don't map to Homebrew idioms, they live in `KandeloFormulaSupport` and are documented. +4. **Owed upstream `brew`-source verification:** `skopeo`/`github_packages.rb`; `pr-upload`/`pr-pull` standalone + synthetic-tag handling; GHCR path derivation for the tap; whether `brew bottle` has non-tar.gz flexibility (settles lazy option 2). +5. **Lazy granularity:** option 3 now; measure whole-bottle fetch cost → option 2 if too heavy. + +## 5. Forward execution plan (bead-ready work items) + +Tracks A/C/D can proceed in parallel; **Track B (brew-in-Kandelo) is the long pole** and gates kd-yuef + retiring `packages/registry`. + +### Track A — Tap migration (greenfield; parallelizable) +- **A0. Tap layout spec.** Decide the Homebrew-idiomatic layout: formula `install` builds from source via `KandeloFormulaSupport`; where `package.toml`/`build.toml` data and demos land (fold into formula attrs / a Kandelo-namespaced sidecar / tap `test`). Write it into the tap `README`/`Kandelo/`. *Accept:* a documented layout + one converted exemplar formula. **Reconcile with #806/#805 first.** +- **A1. Convert `ruby.rb` (instance #1).** Whole-move PR #814's `ruby.rb` + `packages/registry/ruby/` into a real from-source tap formula; open a **tap PR**; close/redirect #814. *Accept:* `brew install ruby` formula builds ruby.wasm from source under the tap's CI (Node smoke: psych/YAML round-trip); nothing ruby lands in main. +- **A2. Migrate the package waves.** Convert the remaining fixture formulae + recipes (perl, cpython, erlang, php, the CLI/dependency-root waves) to tap formulae; one tap PR per package (or per small wave). *Accept:* each builds from source under tap CI + Node smoke; main fixture entry removed. +- **A3. Wire "main pulls from the tap."** Make main's rootfs/VFS/CI builds fetch+build from the tap instead of `packages/registry`. *Accept:* rootfs + a language VFS image build green while sourcing the tap; begin retiring `packages/registry` copies. (Full retire gated on Track B.) +- **A4. Platform PRs shed formulae.** Rebase #819/#817/#816/#811/#812/#795 to drop formula-file edits (now tap-owned); keep their platform/CI content. *Accept:* those PRs touch no `Formula/*.rb`. + +### Track B — brew-in-Kandelo integration (the critical path) +- **B1. Gap analysis (assess-first).** Determine the true state of `brew --version/config/doctor/tap/install` inside Kandelo (Node + browser), from the `target/kd-sqw-homebrew/` probe + sibling worktrees. *Accept:* an evidence-backed ledger of what works/fails + the single hardest blocker with a reproducer. **Use the Appendix prompt.** +- **B2. Drive `brew install ` end-to-end.** Root-cause the blockers from B1. *Accept:* guest `brew install ` succeeds in Kandelo on Node (browser status recorded), reviewable PR. +- **B3. VFS-build-via-brew-in-Kandelo.** Replace the link-manifest replay: host runs Kandelo → brew installs into the canonical prefix → `saveImage()` (heavy files as lazy refs to the published bottle). *Accept:* one demo VFS image built this way, boots on Node + browser. + +### Track C — Publish pipeline (GHCR parity; parallel to B) +- **C1. Tap CI: build → skopeo/pr-upload publish → tap commit,** on the PR→merge split (`pr-upload --upload-only` seed hatch). *Accept:* one formula's bottle published to `ghcr.io/automattic/kandelo-homebrew/...` from tap CI; remote-policy patrol pass. +- **C2. Tag/ABI + `:any` + in-keg `.kandelo/` receipt** wired into `brew bottle` output. *Accept:* a wasm32_kandelo (ABI-tagged) bottle with the receipt; guest-resolution **spike** green (or the exact tag fix identified). +- **C3. Lazy option-3 consumption** — lazy-archive fetch+extract of the published tar.gz bottle. *Accept:* a lazy VFS file materializes from a published bottle at runtime. + +### Track D — Cleanup +- **D1. kd-yuef unblock** (gated on B + C): publish cpython/perl/erlang bottles; composites (kd-v3fs) go green. *Accept:* python/perl/erlang-vfs pass. +- **D2. Doc-correction:** fix PR #823 doc + the kd-1i0u note to match all §2 decisions (guest install in-scope; `:any`; GHCR parity; supersede "don't adopt test-bot/pr-pull"; pour = brew-in-Kandelo; lazy option 3; tap-owns-packages). *Accept:* doc merged/updated. + +**Suggested order:** B1 (assess) + A0/A1 (tap layout + ruby) in parallel → A2/A3 + C1/C2 → B2/B3 → D1 → D2. + +## 6. Appendix — brew-in-Kandelo assessment prompt (paste into a fresh worktree agent) + +> You are driving brew-in-Kandelo to a usable end-to-end state — the critical path for Kandelo's Homebrew plan. Real upstream Homebrew runs INSIDE Kandelo (the kernel sandbox is the trust boundary) to install packages at VFS build time and at runtime. READ FIRST: /Users/brandon/src/kandelo/CLAUDE.md; docs/plans/2026-07-05-homebrew-handoff-record-and-execution-plan.md; docs/plans/2026-07-05-homebrew-bottle-publishing-design-checkpoint.md; docs/plans/2026-06-18-homebrew-runtime-bringup-design.md; docs/plans/2026-06-18-homebrew-portable-ruby-strategy.md. Follow the worktree policy, verification gates, and fork-instrument path (no Asyncify). KNOWN-GOOD (do not re-litigate): Ruby GC root-visibility merged (PR #718); psych/YAML works (PR #814); brew-config Ruby traps closed. A guest-brew boot probe exists at target/kd-sqw-homebrew/; live work is in sibling worktrees — find and read the latest before assuming. PHASE 1 (deliver before changing code): exercise `brew --version/config/doctor/tap` and `brew install ` end-to-end on Node and (as feasible) browser; produce an evidence-backed ledger of what works/fails (reproducers + artifacts) and the exact remaining work, plus whether VFS-build-via-brew-in-Kandelo is prototyped anywhere. PHASE 2: root-cause the highest-value blockers (no demo shortcuts, no masking). ACCEPTANCE: EITHER guest `brew install ` works end-to-end on Node (browser status recorded) with a reviewable PR to Automattic/kandelo, OR a precise gap analysis with the hardest blocker isolated by a minimal reproducer. + +## 7. Pointers + +- Design rationale: `docs/plans/2026-07-05-homebrew-bottle-publishing-design-checkpoint.md`. +- Runtime/pour design: `docs/plans/2026-06-18-homebrew-runtime-bringup-design.md`, `2026-06-18-homebrew-portable-ruby-strategy.md`, `2026-06-19-ruby-wasm-gc-root-visibility-design.md`, `2026-06-18-homebrew-vfs-builder-design.md` (Open Q #1 = link-manifest generator, obsoleted by brew-in-Kandelo). +- Lazy files: `docs/architecture.md` §Lazy Files/§VFS Images; `docs/package-management.md` §Rootfs lazy binary manifests. +- Code: `tools/xtask/src/homebrew_vfs_build.rs` (link-manifest replay, being replaced), `tools/xtask/src/homebrew_vfs_plan.rs` (`DEFAULT_PREFIX`), `tools/mkrootfs/`, `packages/registry//` (recipes → migrate to tap), memfs lazy files in `host/src/*`. +- Repos: `Automattic/kandelo` (platform), `Automattic/kandelo-homebrew` (tap; currently `Formula/hello.rb` + `Kandelo/` support). +- Prior research: PR #823. Merged Ruby: PR #718. Redirect: PR #814.