Repository navigation
RFC: web/wasm leaves the CLI core (RFC #406, web phase) #466
Description
Status: revision 4. D1–D13 approved by the owner; implementation planned with the review qualifications below.
Revision 4 (owner decision, 2026-09-27): retain watch and live reload for browsers. This supersedes revision 3's removal of watch mode.
- Keep
labelle run --watchthrough the web provider: generic watching and rebuild supervision in core; HTTP serving and browser reload in the provider. - Require explicit watch capability, consistent successful-output publication, session-change diagnostics and child cleanup (§3.4 and §4). Desktop/native watch support is outside this migration.
- Restore the separate watch-supervision PR A2 and browser acceptance tests. Carry forward the prebuild ignore/replan fix described in wasm serve --watch: refresh the watcher's ignore list when prebuild outputs change #463; its earlier closure does not remove that requirement.
- The legacy
labelle wasm serve --watchcommand still goes away withlabelle wasm; browser watch useslabelle run --watch. - D1–D13 are accepted (§7). See the D1–D3/D5–D13 approval and D4 approval.
Revision 2's environment rules, FP-before-3.0 release order and review acceptance requirements remain in effect.
This is the web phase of RFC #406. v2.0.0 delivered the provider infrastructure and moved Android out of the core. This RFC plans the next step: moving web/wasm out of the CLI core into the web provider (labelle-toolkit/labelle-web), the same way Android moved into labelle-android.
The research is read-only, against origin/main: cli 8188800 (v2.0.1), web 1d669ee (v0.2.0), assembler b239c9d. File sizes are line counts.
0. Findings that shape the plan
-
--platform=wasmalready requires thewebprovider. web v0.2.0 already replaces core serve and export whenever it is pinned:- web's
replace run(serve) andreplace bundle(export) hooks take over from core. - The legacy
labelle wasm serve|exportis refused whenever a run replacement exists (pipeline/args_resolve.zig:236-257,pipeline/install.zig:203-206). - So core serve and export only run for a provider that owns
wasmbut has noreplace run. In practice that is only the CLI's own fixture,test/fixtures/wasm-provider. - Core
serve/http.zig,serve/server.zig,export.zigandpipeline/export_output.zigare therefore dead weight for real users. web already carries copies, "extracted from labelle-cli at 5eccdbc":src/provider/serve.zig(2247 lines) andsrc/provider/export.zig(1018).
- web's
-
Core still does four wasm jobs the provider can't take over today:
- defaulting to ReleaseSafe (
pipeline.zig:263-264); - activating emsdk after the fingerprint pass (
pipeline/generate.zig:190-218, which includes a Python preflight); - the managed-emsdk environment for the core
zig build(runner.buildWasmEnv,runner.zig:22-90); --watch: watcher, rebuild loop and live reload (pipeline/run.zig:218-317). Generic watching stays in core; browser live reload moves to web.
Each of these is a contract gap (§3).
- defaulting to ReleaseSafe (
-
All three backends already honour an external
EMSDKand otherwise fall back to theemsdkZig dependency:- bgfx
backend.hook.zig:388-424, sokol:397-430, raylib:107-142. - The assembler's
ensureEmsdkActivatedpreflight short-circuits whenEMSDKis set (codegen/emsdk_preflight.zig:26-36). - So one generic "provider contributes env to the core compile" mechanism lets web use a single shared emsdk for every backend, with no emcc knowledge in core.
- bgfx
-
Flying Platform already works around the core activation. Its
ci.yml:363-386anddeploy-pages.yml:78-84run./emsdk install/activate 4.0.9by hand on everyzig-pkg/*/emsdk: the graph holds two emsdk hashes, and core activates only one (emsdk_activate.zig:151-230). FP also passes--optimize=ReleaseSafeexplicitly. So FP keeps building while core loses wasm, which makes this train lower-risk than Android's. -
The file watcher is platform-neutral code wearing wasm names. Retain and rename its generic parts; remove HTTP coupling.
serve/tree.zig,serve/baseline.zig,serve/watch.zig,serve/cancel.zig,pipeline/watch.zig(WasmRebuildCtx) andpipeline/watch_replan*.zignever touch emcc.- Only the HTTP live-reload endpoint is web-specific.
- Providers can't build, so the rebuild loop stays in core.
-
Python has a second, non-wasm user.
.prebuildsteps get the managed interpreter throughpython_provision.autoWireEnv(pipeline/install.zig:51-75). -
web v0.2.0 is capped at
>=1.0.0 <1.2.0, so it gets the 1.1.0 wire: notarget_dirand norunoptions. -
Allowlist count: the guard's
allowed_fileshas 53 entries: 52 legacy files, includingstb_image.h/.c, plus the guard itself. -
Studio (out of scope for this train) depends on core wasm features:
doctor --json'swasmcapability (ToolchainGate.tsx,services/doctor.ts,src-tauri/src/lib.rs);labelle install python;labelle build --platform=wasm.
1. Inventory (labelle-cli origin/main)
1a. Toolchain provisioning
| File | What it does | Fate |
|---|---|---|
emsdk_toolchain.zig (777) |
Managed emsdk: precedence LABELLE_EMSDK > --emcc > .emsdk_version > engine-derived > 4.0.9 pinned commit; ensureInstalled, cmdInstallEmsdk, cmdEmsdkWhich |
→ web |
emsdk_cache.zig (177) |
Layout ~/.labelle/emsdk/<ver>/… |
→ web (cache_dir) |
emsdk_activate.zig (759) |
In-place activation of the fetched zig-pkg emsdk (dep-hash lookup, fs2 lock) | → web (activate every zig-pkg/*/emsdk) |
python_provision.zig (324) |
python-build-standalone 3.11.9, pinned sha256; autoWireEnv |
Stays generic (D2); scrub the wording |
install.zig:40-67 |
install python, install emsdk |
emsdk → web |
cli.zig:42-43,151-157 |
labelle toolchain emsdk |
→ web command |
launcher_manifest.zig:19-23 |
.emsdk_version |
→ providers/web.json |
args.zig:13,496-530 |
--emcc |
Delete (D9) |
docker.zig:5-54,230-313 |
Wasm container branch, STOPSIG stdlib sed | Delete (D5) |
1b. How labelle build --platform=wasm works today
- Resolve: the target is resolved to the web provider, and
parsed.platform = .wasmis kept, because the assembler schema still needs it. - Compatibility:
compatibility.zig:10-14hard-codes which backends support wasm. - Generate: defaults to ReleaseSafe; runs the ASTC prepass for
.web; runs the assembler; thenfixFingerprintsfetches emsdk intozig-pkg. - Wasm-only block (
generate.zig:190-218): Python wiring and preflight, then emsdk version resolution and activation. - Build:
runner.buildWasmEnvsets EMSDK, EM_CONFIG and PATH; then corezig build; then web'safter buildshell hook. - Run and bundle: web's replacements. The core fallback branch (
pipeline/run.zig:194-318) runs only when there is no replacement.legacyRunBranch(.wasm) = trueexempts wasm fromNoRunReplacement.
1c. Serve, watch and live reload
| File | Fate |
|---|---|
serve.zig, serve/server.zig (213), serve/http.zig (631) |
Delete (web has copies) |
serve/watch.zig, serve/tree.zig, serve/baseline.zig, serve/testing.zig |
Move generic watch code/tests out of serve/; remove wasm-specific names |
serve/cancel.zig |
Retain generic cancellation support outside serve/ |
pipeline/watch.zig (747), pipeline/watch_replan*.zig |
Retain generic rebuild/replan supervision; refresh prebuild steps and ignore paths together |
pipeline/run.zig:34-141 (serveNoBuild), :194-318 (core wasm branch) |
Remove legacy serving; replace wasm-specific watch path with generic provider supervision |
cli.zig/args.zig `wasm serve |
export` parsing and help |
provider_dispatch.zig reserved "wasm" namespace |
Unreserve |
| cli#463 (watcher ignore list) | Its fix is required in A2 despite the earlier closure: replan before prebuild and atomically refresh steps plus ignore paths |
1d. Export and bundle
export.zig(884) andpipeline/export_output.zig(209): delete. web's export supersedes them and adds shell staging and its own destructive-output guard.
1e. ASTC for web
project_configAssetCompression.web/formatFor(.wasm)is shared schema with the assembler. It stays until the Platform-enum phase.- The ASTC prepass is already generic (Move compressed-texture capabilities (backend × platform ASTC table) out of the CLI into backend manifests #407/feat(astc): read upload capabilities from backend manifests #428).
1f. Config, args, help, doctor
help.ziglegacy wasm section.doctor.zig:checkPython,checkEmsdk, and the--jsonwasmcapability (Studio).runner.buildWasmEnv.compatibility.zigwasm whitelist and hint text.- Comment-only sites in
prebuild.zig,material_toolchain.zig,provider_hooks.zig,assembler_proc.zig,pipeline/testing.zig.
1g. Tests and CI
- ~40
wasm serve/exportspecs inargs_tests.zig. - Module tests in export, serve and watch.
test/provider_hooks_e2e.py:434-460;test/provider_targets_e2e.py:171-194,309-317.test/fixtures/wasm-provider.- CI:
ci.yml:166-276(Docker wasm);toolchain.yml:17-19,73-121(Python from zero, doctor-json contract, from-zero wasm).
2. The web provider today (v0.2.0)
- Manifest:
namespace = "web",targets = {"wasm"},command_contract = ">=1.0.0 <1.2.0". - Hooks:
shell(after build): stages the loader and stampsdata-wasm-bytes;serve(replace run): serves, with no watch;export(replace bundle).
- Commands:
serveandexportact on existing output (--port,--no-open,--output,--zip,--platform). - Settings:
{build_dir, port, open_browser}, with no schema version. - Runtime: IndexedDB storage and the shell.
- Duplicates core: serve (http, server, cancel), export plus ZIP plus wasm-opt, and the output guard.
- Missing:
- emsdk provisioning and pin;
doctor;- a default optimize;
run.args/target_dir(range capped below 1.2);- a settings schema version and export settings.
3. Contract gaps: an additive contract 1.3.0
| Need | Expressible today? | Proposal |
|---|---|---|
| Toolchain env for the core compile (EMSDK, EM_CONFIG, PATH) | No: a hook's env dies with the hook | env_file on before generate, after generate and before build hooks |
| Persistent toolchain cache (~1 GB) | Only by re-implementing LABELLE_HOME resolution |
cache_dir |
| Default optimize per target | No: hard-coded in core | Manifest .target_defaults (not wire) |
| Browser watch/reload | No generic rebuild notification | run.watch plus explicit run-replacement .watch = true capability |
Optimize and run.args |
Yes, on 1.2.0 | web moves to ≥1.2 |
| Progress JSON | The contract already requires NDJSON on stdout under --progress=json |
web prints diagnostics to stderr then |
1.3.0 keys: supported_versions = {1.3.0, 1.2.0, 1.1.0, 1.0.0}, with the new keys gated by wire version.
3.1 cache_dir (PR A1)
- Required on 1.3.
- Absolute
<LABELLE_HOME>/providers/<canonical-provider-id>/, where the canonical id is the pinned repo identity (github.com/<owner>/<repo>, normalised), not the project's plugin alias. Two projects that alias one provider share its cache. - Created by the CLI, and persistent across versions and projects.
- The provider owns the layout inside. It must key toolchain installs by host (os/arch) and SDK identity (version plus pinned commit), and make concurrent installation safe: a lock plus atomic rename of a completed install. Two builds installing the same emsdk at once must end with one valid install and no partial tree.
3.2 Environment contributions: env_file (PR A1)
Where it applies. env_file is non-null on the before generate, after generate and before build hooks: an absolute, empty path the hook may write. It is null on all other hooks.
- Contributions accumulate: every later
ziginvocation in the same build or rebuild sees the merged result of all contributing hooks that have already run. - That covers the generation-time fingerprint pass (which runs after the assembler, inside the generate step, so
before generatecontributions reach it), the compile, and the processes listed under "Scope" below. - Phase meaning is unchanged:
before buildstill runs just before the compile.
File format. Strict JSON, decoded with unknown fields rejected:
{ "set": [ { "name": "EMSDK", "value": "/abs" } ], "path_prepend": [ "/abs/dir" ] }Reserved names. Instead of a LABELLE_* prefix ban, a fixed list of CLI-owned names can't be set:
PATH(only viapath_prepend),ZIG_GLOBAL_CACHE_DIR,ZIG_LOCAL_CACHE_DIR;LABELLE_HOME,LABELLE_CONTEXT,LABELLE_OFFLINE,LABELLE_ZIG,LABELLE_ASSEMBLER, and everyLABELLE_*name the CLI itself reads (kept as a single table inprovider_contract.zig, with a guard test).
Other LABELLE_* names, such as LABELLE_SDL2_LIB for the SDL phase, are allowed. Names must match [A-Za-z_][A-Za-z0-9_]*.
Merge rules, deterministic:
- Precedence: a hook contribution overrides the inherited environment, because a provider that provisions a toolchain must win over a stale shell
EMSDK. A provider that wants to honour a user's inherited value reads it from its own environment and writes that value back. - Order: contributions are applied in hook execution order (hooks are already totally ordered: phases, dependencies, then id).
- Conflicts: two hooks setting the same name to different values is an error naming both hooks. Setting the same value is fine.
- Windows: names are compared case-insensitively for both conflicts and override (
Path/PATH,EMSDK/Emsdk), and the inherited spelling of the key is kept. - PATH order:
path_prependentries go in hook order, each hook's list in its given order, all in front of the inherited PATH. Duplicates are removed keeping the first. Entries must be absolute; a relative entry is an error.
File states:
- Missing: no contribution. This is normal.
- Empty or malformed: the build fails before compiling and names the hook and the parse error.
- Written but the hook failed: the file is ignored and the hook failure stands.
Freshness. The environment is built fresh on every build, from the inherited environment plus the contributions of the hooks that ran in that rebuild. A hook removed by a replan leaves nothing behind, and env_file paths are per-invocation temp files deleted afterwards.
Scope. The merged environment goes to:
- the core
zig buildcompile; - the generation-time
zig build --list-stepsfingerprint repair: see "Ordering" below; - hooks and replacements that run after the contributing hook in the same invocation. That covers later generate hooks,
after build, thereplace build/replace bundle/replace runprocesses, and theafter runhooks, so a replacement build sees the same toolchain.
It does not reach provider host-tool compilation (zig build install-provider for a provider's own tool). That runs with the plain inherited environment plus the managed host Zig, so a provider can't change how other providers' tools are built.
Ordering: the earlier toolchain consumer. Generation runs zig build --list-steps for fingerprint repair before the build phase, and that can already evaluate backend and toolchain configuration. So web's toolchain hook is a before generate hook: its EMSDK/EM_CONFIG/PATH contribution reaches the fingerprint pass and every later zig invocation.
- The package mode (
source = "package", activating the fetchedzig-pkg/*/emsdk) needs the fetched tree. It runs as anafter generatehook instead, and its contribution applies from the compile onward. - Clean-cache acceptance exercises the fingerprint pass under both modes (§8). The final compile is not assumed to be the first toolchain consumer.
3.3 Target defaults (PR A1, manifest, not wire)
-
The manifest declares, for example,
.target_defaults = .{ .{ .target = "wasm", .optimize = .ReleaseSafe } }. -
Only the target's owner (the provider that declares the target in
.targets) may declare defaults for it. A default on a target the provider doesn't declare is a manifest error, and two declarations for one target are an error. -
An explicit CLI flag always wins (
--optimize,--release). The default applies only when the flag is absent, and the wireoptimizereports the effective value. -
Recompute effective defaults on every allowed build replan; if the effective optimize affects the running replacement, apply the restart rule below.
3.4 Browser watch (PR A2)
labelle run --watch --platform=wasmuses a long-lived web run replacement. Require both contract ≥1.3 and an explicit.watch = truecapability on that replacement. Reject unsupported replacements with an actionable diagnostic; protocol negotiation alone is insufficient. Desktop/native watch is outside this migration.- On wire 1.3,
run.watchisnulloutside watch sessions or an object with absolutegeneration_fileandoutput_dirpaths. Older wire versions omit it. Core initializes generation to 0 before starting the replacement; the provider serves the published output and polls generation to notify browsers to reload. - Publication and notification are separate. Build into isolated staging, finish all after-build hooks, then publish a complete generation and switch the served output consistently before advancing generation. The provider must never serve partially rewritten build output. Preserve the last successful output and generation on failure. Validate the publication mechanism on Windows as well as POSIX; a counter alone is not publication.
- Code/assets trigger rebuilds. Build-only configuration changes may replan atomically. Changes affecting the running replacement require
restart labelle run --watch: <what> changedand publish nothing: provider package/version/pin, replacement hook, backend, target, output directory, effective optimize, replacement settings or watch capability. - Replan changed project configuration before its prebuild phase; swap prebuild steps and their ignore set together (wasm serve --watch: refresh the watcher's ignore list when prebuild outputs change #463). Construct a fresh environment for each rebuild (§3.2).
- The running replacement retains its session configuration; never silently combine an old server with incompatible new build plans.
4. Target architecture
labelle build --platform=wasm -> before generate web/toolchain : ensure emsdk (cache_dir) -> env_file {EMSDK,EM_CONFIG,PATH}
-> assembler generate; zig-pkg fetch + fingerprint pass (sees the env)
-> core zig build (-Doptimize from --optimize | target_defaults) with env merged
-> after build web/shell : stage + stamp index.html
labelle run --platform=wasm [-- --port=N --no-open]
-> same build, then replace run web/serve (blocks)
labelle run --platform=wasm --watch -> same initial build, then web/serve + core rebuild supervision
labelle bundle --platform=wasm -> same build, then replace bundle web/export
labelle web serve|export|doctor|toolchain [args]
labelle doctor -> core checks + web/doctor (emsdk, python)
Stays in core (generic), and why:
- File watching, debounce, rebuild/replan orchestration and successful-build notification, with browser support delivered through web (§3.4).
- Interrupt handling and session supervision. Server exit stops the watcher and cancels/reaps any in-flight build before session teardown. Forward termination to the child process group or Windows job object, then enforce cleanup; no child outlives the command. Only a clean status-zero replacement exit runs
after run; signal death, timeout, forced termination and non-zero exit skip it. Cleanup always runs. Test interruption during provisioning, compilation and after-build hooks, not only an idle server. labelle install python+autoWireEnv(D2)..prebuildneeds it. Provider commands and hooks receive the same managed-Python PATH wiring.- The 1.3 keys, which are generic; the SDL and iOS phases reuse
env_fileandcache_dir.
Moves to labelle-web:
- the emsdk pin, fetch, verify, activate, cache and
which; - the in-place zig-pkg activation, as a fallback mode that activates every
zig-pkg/*/emsdk(this fixes FP's two-hash workaround); - HTTP serve and live reload;
- export;
- doctor checks for emsdk and python-for-emsdk;
- the ReleaseSafe default, as manifest data;
- the wasm spike and segfault docs.
Deleted without replacement:
labelle wasm serve|export(RFC: package-provided commands and build hooks (agnostic CLI, rake-with-gems model) #406: no legacy aliases);--emcc,LABELLE_EMSDKand.emsdk_version(useEMSDKorproviders/web.json);--dockerfor provider targets (D5);- the compat whitelist (D13).
5. PR sequence
| # | Repo | Title | Depends on |
|---|---|---|---|
| A1 | cli | Contract 1.3.0 context: cache_dir, env_file (full rules §3.2), manifest target_defaults |
none |
| A2 | cli | Generic browser-watch supervision, explicit capability, publication and session-change rules (§3.4) | A1 |
| W | labelle-web | v0.3.0: contract ≥1.3, toolchain before-generate hook (+ after generate package mode), doctor/toolchain commands, settings v1, browser live reload |
A1, A2 |
| B | cli | Remove web/wasm from core, refuse --docker for provider targets, shrink the guard |
A1, A2, W; FP migrated (F) before B is published |
| S | labelle-assembler (optional) | Preflight message points to the web provider; backend-supports-wasm check if absent | none |
| R | labelle-registry | Add the web 0.3.0 record | W |
| F | flying-platform-labelle | Pin web 0.3.0, drop the manual emsdk loops | W, R |
Backends (bgfx, sokol, raylib) need no PR: their wasm CI uses plain zig build with the emsdk dependency and honours EMSDK.
PR A1: contract 1.3.0 context (additive)
Files:
provider_contract.zig: the version, thecache_dirandenv_filekeys, and validation and serialization.provider_hooks.zig:cache_root, per-hookenv_fileallocation, read-back and merge (§3.2).provider_manifest.zig:target_defaults.pipeline.zig:effective_optimizefromtarget_defaults.pipeline/generate.zig: the accumulated env reaches the fingerprint pass.pipeline/build.zig: apply the merged env to the compile.- Comment scrubs in
prebuild.zigandmaterial_toolchain.zig. - Docs.
Tests:
-
Decoder: 1.3 accepts the keys and ≤1.2 rejects them; negotiation.
-
Env merge: a unit table covering reserved names, conflicts, Windows case-insensitivity, PATH order and dedup, and missing/malformed files.
-
e2e:
- a fixture
before generatehook's env reaches the fingerprint pass and the fixturebuild.zig; - a hook removed between two builds leaves no value behind;
target_defaultsgives ReleaseSafe, and an explicit--optimizewins.
They run on Windows too.
- a fixture
Guard: 53 → about 50 (recount in the PR).
Release: CLI 2.1.0, after A1 and A2 (D1).
PR A2: browser-watch supervision (additive)
- Add the version-gated
run.watchcontext and explicit run-replacement watch capability. - Retain generic watcher/replan/cancellation code under neutral names; remove its HTTP/server dependency.
- Implement §3.4 publication and configuration rules, including wasm serve --watch: refresh the watcher's ignore list when prebuild outputs change #463's prebuild/ignore refresh.
- Keep rebuild state separate from the running session; cancel/reap workers before freeing session state.
- Test capability rejection, successful notification only after after-build hooks, failed-build output preservation, configuration changes, fresh rebuild environments, server exit and interruption during each build phase. Cover Windows process cleanup and publication.
- Release with A1 in CLI 2.1.0; keep these as separate reviewable PRs.
PR W: labelle-web v0.3.0
Manifest:
command_contract = ">=1.3.0 <1.4.0", plustarget_defaults.- Commands:
serve,export,doctor,toolchain which|install [<ver>]. - Hooks:
toolchain(before generate: managed/root/inherited modes),toolchain-package(after generate:source = "package"mode),shell(after build),serve(replace run,.watch = true),export(replace bundle).
src/provider/emsdk.zig, ported from the CLI. It picks emsdk in this order:
- an already-set
EMSDKwith an existing emcc, passed through; - settings
emsdk.root; - managed
<cache_dir>/emsdk/<host>/<version-and-commit>/(pinned commit, fs2 lock); emsdk.source = "package", which activates everyzig-pkg/*/emsdk.
It writes env_file with EMSDK, EM_CONFIG and a path_prepend of upstream/emscripten.
Other changes:
- Python: found on PATH, where core wires the managed one; if missing, it fails with "run
labelle install python". doctor: its--jsonkeeps Studio's{id:"wasm", items:[python, emsdk]}shape (D7).serve: readsrun.args(--port=,--no-open). In watch mode it serves only the consistently publishedrun.watch.output_dir, pollsgeneration_fileand reloads browsers after a successful generation change. Remove duplicated rebuild/file-watching code; core owns rebuilds.- Settings v1, strict:
{ "schema_version": 1, "port": 8080, "open_browser": true, "build_dir": null, "emsdk": { "version": "4.0.9", "source": "managed", "root": null }, "export": { "platform": "none", "zip": false } }
Tests:
- Host unit tests: version resolution,
env_file, two-hash package mode, the generation-file poll. - An e2e against CLI A1/A2 with a fake emsdk.
- A new wasm-from-zero job on ubuntu, macOS and Windows: clean HOME,
labelle init,local:web,build --platform=wasm; assert the wasm magic anddata-wasm-bytes. It moves from the CLI'stoolchain.yml.
Release: GitHub Release v0.3.0, plus the codeload sha256.
PR B: remove wasm from core (breaking)
Deleted:
emsdk_activate.zig,emsdk_cache.zig,emsdk_toolchain.zigexport.zig,pipeline/export_output.zigserve.zig,serve/http.zig,serve/server.zigtest/fixtures/wasm-provider/- the wasm spike and segfault docs, moved to labelle-web
Edited:
cli.zig,args.zig,args_tests.zig,help.zig: drop the wasm subcommands,toolchain emsdk,--emccand the legacy help.install.zig: removeinstall emsdk.doctor.zig: remove the emsdk/python checks;--jsonper D7.runner.zig: removebuildWasmEnv.pipeline.zig,pipeline/generate.zig:190-218,pipeline/run.zig,pipeline/args_resolve.zig: remove the wasm branches.pipeline/install.zig:legacyRunBranchkeeps only.ios.provider_dispatch.zig: unreservewasm.docker.zig: refuse--dockerfor provider targets.compatibility.zig: delete the whitelist (D13).python_provision.zig: scrub the wording, and wire the managed interpreter unconditionally when it exists.launcher_manifest.zig: removeemsdk_version.- Comment scrubs in the files listed in §1f.
- New
docs/migrating-to-3.0.md. - CI: the Docker job becomes a desktop cross build;
toolchain.ymldrops the emsdk and wasm jobs.
Tests:
labelle wasm servegives the unknown-command diagnostic, and nothing is generated.NoRunReplacementapplies to wasm.- The guard passes with the shrunken list.
- The provider e2es run on 3 OSes.
Clean-HOME acceptance (fresh FP clone, empty HOME, no siblings, dev CLI, web as local: and then pinned):
providers fetch, thenbuild --platform=wasmwith no--optimize. Expect-Doptimize=ReleaseSafe, no manual emsdk step, and emcc under<cache_dir>/emsdk/<host>/<version-and-commit>/.run --platform=wasm --watch: edit an asset and observe a browser reload; break a build and verify the last successful output remains served.bundle --platform=wasm --output dist, then FP'ssmoke.py --soak-seconds 150.doctorshows theweb doctorrows.LABELLE_OFFLINE=1reuses the cached toolchain.- Windows: steps 1–3, after
labelle install python.
Guard: expected to shrink toward 32 after generic watcher names and web policy are removed from the allowlist. Retained generic watch files are not deleted; recount in A1, A2 and B.
Release: CLI 3.0.0 (D1).
PR S (assembler, optional)
- The preflight message points to
labelle web doctor. - Verify whether the assembler already refuses a backend without
.platforms.wasm; if not, add the check. It replacescompatibility.zig:10-14.
PR F (Flying Platform)
- Pin web 0.3.0:
providers resolve, then--accept. - Delete the manual emsdk loops in
ci.ymlanddeploy-pages.yml. - Add the actions cache for
<LABELLE_HOME>/providers/<canonical-provider-id>.
6. Release train (revised: FP migrates before the removal is published)
- CLI A1 and A2, then release 2.1.0 (additive).
- labelle-web v0.3.0 against released 2.1.0, then its labelle-registry record.
- Assembler S, if taken, but its D13 verification is required before B (§7).
- FP F, committed and green on CLI 2.1.0 + web 0.3.0: the manual emsdk loops are removed and the clean-machine acceptance passes (§8). FP installs "latest", so the committed migration must be in place before 3.0 is published. Testing an unpublished combination is useful but doesn't close that upgrade window.
- CLI B, then tag v3.0.0; verify R2 and
latest.txt; FP's next CI run is on 3.0.
Why 3.0.0: B removes CLI features:
- the
wasmsubcommands; install emsdkandtoolchain emsdk;--emcc,LABELLE_EMSDKand.emsdk_version;--docker --platform=wasm;- core auto-activation;
- the
doctor --jsonwasm rows.
A1 and A2 are additive, so 2.1.0.
7. Approved decisions
All D1–D13 are accepted by the owner with these qualifications. D4 supersedes revision 3.
| Decision | Approved outcome |
|---|---|
| D1: versioning | CLI 2.1.0 (A1 + A2), web 0.3.0 + registry, FP committed and validated, then CLI 3.0.0 removal (§6). |
| D2: Python | Keep install python and generic managed-Python wiring in core. Provider commands and hooks get the same PATH wiring as prebuild scripts. |
| D3: environment | Accept env_file with the complete §3.2 rules and clean-cache verification of the earlier fingerprint pass. |
| D4: browser watch | Keep labelle run --watch for browsers. Core watches/rebuilds; web serves/reloads. Explicit capability, consistent publication, restart diagnostics and cancellation rules apply (§3.4/§4). Native/desktop watch is outside this migration. |
| D5: Docker | Refuse --docker for provider targets in this migration; use a desktop CI lane. Generic container execution is deferred. |
| D6: optimize | Target-owner-only manifest defaults; reject duplicates; explicit CLI flags win; recompute on replan with session-change safeguards. |
| D7: doctor JSON | Aggregate validated provider capability objects into one core-owned JSON document. The web provider owns wasm; no core hardcoding. Duplicate IDs fail naming both providers; provider failures produce an error capability with ok:false. Capture provider JSON or use a CLI-owned file so stdout remains one final document. |
| D8: cache | New persistent cache keyed by canonical provider identity, with host/SDK identity and concurrent installation handled by provider layout. Do not migrate or delete the old cache automatically; document optional cleanup. |
| D9: escape hatches | Remove --emcc, LABELLE_EMSDK, .emsdk_version; use EMSDK and provider emsdk.root/emsdk.version. |
| D10: project scope | Provider toolchain commands remain project-scoped until a separate projectless bootstrap feature. |
| D11: init defaults | Desktop + bgfx; no provider proposed by default. |
| D12: Studio | Not a release gate. Track provider pin, doctor contract and toolchain-command migration as follow-up work. |
| D13: compatibility | Remove the CLI wasm backend whitelist only after verifying equivalent assembler/manifest rejection, adding it if absent. Verification is mandatory even when no assembler change is required. |
Document the breaking changes in docs/migrating-to-3.0.md.
8. Risks and acceptance
Completion criterion: clean-machine web builds through provider-owned policy. The shrinking allowlist (starting at 53; intermediate and final counts to be verified) is supporting evidence, not the acceptance criterion on its own.
Acceptance for A1/A2/W/B, in addition to the §5 lists. Each item is a test that asserts the path taken:
- Clean-cache toolchain bootstrap in a controlled environment: clean HOME alone isn't enough, because an inherited
EMSDK, PATH or installed Python can hide missing provisioning.- Run with
EMSDKunset, PATH scrubbed of emcc and python, andLABELLE_HOMEempty; assert the provider provisioned emsdk andlabelle install pythonwas required and used. - Separately: with an external
EMSDKset, the provider passes it through and doesn't download. - With Python missing, the failure message names
labelle install python.
- Run with
- Fingerprint/toolchain ordering: managed, root and inherited modes contribute before the generation-time
zig build --list-steps. Package mode contributes after generation, so test the earlier fingerprint pass on a clean cache explicitly; if it requires an activated toolchain, resolve that ordering before shipping. Do not assume an after-generate contribution reaches an earlier pass. - Browser watch: require explicit capability as well as the negotiated wire version. Verify asset/code rebuilds notify the browser only after all build hooks succeed.
- Failed-build preservation: while a rebuild writes staging output, the server continues serving a consistent successful generation. A compile or after-build failure changes neither published output nor generation. Verify successful publication on Windows and POSIX.
- Configuration changes during watch: build-only replans refresh environment, prebuild steps and ignore paths together before prebuild. Changes affecting the running replacement yield a restart diagnostic and publish nothing.
- Server exit: stops the watcher and cancels/reaps an in-flight rebuild before session teardown.
- Cancellation: interrupt during provisioning, during the compile and during an
after buildhook. Each time the in-flight child is cancelled and reaped, and no orphan process remains (POSIX process group; Windows job object, asserted on windows-latest). The existingrunoutcome rules hold: a replacement exiting 0 getsafter run; a signal, timeout or non-zero exit skips it; cleanup runs in both cases. - Concurrent toolchain installation: two builds that start the same emsdk install at once end with one valid install, no partial tree and both builds succeeding.
- Offline cache miss:
LABELLE_OFFLINE=1with no cached emsdk fails clearly inweb/toolchain, naming the missing version; with a cached one it succeeds with no network. - Progress JSON: under
--progress=json, no web hook or provider-doctor bytes reach stdout outside core's NDJSON/JSON document.
Risks:
-
FP CI and Pages: low risk. FP already activates by hand and passes
--optimize, and it migrates before 3.0 is published (§6). After F, cache<LABELLE_HOME>/providers/<canonical-provider-id>in actions. -
Non-FP web users on 0.2.0 with CLI 3.0 lose auto-activation. The assembler preflight fails with an actionable message, and the 3.0 guide says to pin web ≥0.3.0.
-
Backend wasm examples and CI: unaffected (plain
zig buildwith the emsdk dependency). -
Studio Play mode: breaks on 3.0 through the
doctor --jsoncapability,LABELLE_EMSDK/install emsdkand the missing web pin. Out of scope; flag it on studio#123. -
Windows:
- emsdk.bat plus the Python bootstrap;
- long zig-pkg paths (managed mode avoids them);
- case-insensitive env keys (§3.2);
- job objects for child cleanup;
- consistent output publication under Windows filesystem semantics.
W's windows-latest from-zero job and A1/A2's Windows e2e cover this.
-
Disk: old and new emsdk caches coexist during migration, about 1–2 GB.
9. Follow-on phases
- iOS (
ios.zig, and the ios branches incli.zig,pipeline.zig,pipeline/run.zig,args_resolve.zig,bundle.zig, andIosConfig): a labelle-ios provider withreplace run(simctl) and possiblyreplace build(xcodebuild), usingenv_fileandcache_dir.legacyRunBranchthen disappears. - SDL provisioning (
sdl_provision.zig, the doctor SDL checks, SDL2.dll staging,wants_sdl2): the SDL backend package gets abefore buildhook on desktop that setsLABELLE_SDL2_LIBviaenv_file, plus anafter buildDLL staging hook. - Backend identity: RFC #406 step 5: make backend identities manifest-declared (drop the core Backend enum) #432, blocked on labelle-assembler#378.
- Platform enum:
project_config.Platform/AssetCompression, a schema shared with the assembler, goes with chore: pin labelle-assembler 0.105.0 #378. - Also open: the
docker.zigxcode cross-build, and the vendoredstb_image.h/.c(upstream text names platforms; exempt them by hash or move them to a package).
Expected allowlist after the web phase (32 candidates; recount after A1/A2/B):
agnostic_guard_test.zig,astc/cmd.zig,cli.zigcli/args.zig,cli/assembler_proc.zig,cli/bundle.zig,cli/compatibility.zig,cli/config.zig,cli/docker.zig,cli/doctor.zig,cli/help.zig,cli/ios.zig,cli/linux_desktop.zig,cli/pack.zigcli/pipeline.zig,cli/pipeline/args_resolve.zig,cli/pipeline/build.zig,cli/pipeline/generate.zig,cli/pipeline/install.zig,cli/pipeline/run.zig,cli/pipeline/screenshot.zigcli/progress.zig,cli/project_config.zig,cli/provider_dispatch.zig,cli/runner.zig,cli/screenshot_format.zig,cli/sdl_provision.zig,cli/status.zigcli/stb_image.h,cli/stb_image_impl.c,cli/update_check.zig,cli/upgrade.zig
Each still names ios, android, sdl, xcode or a backend.
Critical files
labelle-cli:src/cli/provider_contract.zig: wire 1.3.0src/cli/pipeline/run.zig: legacy wasm branch removal and generic provider watch supervisionsrc/cli/pipeline/watch*.zigand generic watcher files insrc/cli/serve/*: retained/renamed under neutral modules; HTTP/server files removed from coresrc/cli/pipeline/generate.zig:190-218andsrc/cli/emsdk_toolchain.zig: the activation and provisioning that movesrc/agnostic_guard_test.zig: 53 initially; recount after A1, A2 and B
labelle-web:src/provider_main.zig,src/provider/serve.zig,plugin.labelle.flying-platform-labelle:.github/workflows/ci.ymlanddeploy-pages.yml(the manual emsdk loops).
Activity
apotema commentedon Sep 27, 2026
Review: approve the direction; revise the contract before implementation
Read-only review against CLI
8188800and the assembler/provider sources referenced by this proposal. The separation is sound: generic orchestration and watching stay in core; web tooling and policy move into the provider. The revisions below should be settled before implementation.Required revisions
Define watched configuration changes. A long-lived replacement server cannot silently keep its old provider/configuration while rebuilds adopt a different provider version, backend, output directory or run replacement. For the first version, allow script/asset rebuilds and atomic build-only replans; require a restart with a diagnostic when changes affect the running replacement. Refresh prebuild steps and their ignore set together (wasm serve --watch: refresh the watcher's ignore list when prebuild outputs change #463). When the server exits, stop the watcher and cancel/reap an in-flight build before finishing the session. Separate arenas address memory lifetime, not this semantic mismatch.
Declare watch support explicitly. Negotiating contract >=1.3 means a provider understands the wire, not that its replacement stays alive and consumes reload notifications. An Android-style deploy replacement could accept 1.3 and immediately exit. Add an explicit watch capability on the run replacement. Keep the generation file, but distinguish reload notification from artifact publication: advancing the counter only on success does not stop the server reading partially rewritten output. Specify last-successful-output serving or another consistent publication policy.
Complete the environment-contribution contract. Section 3 forbids
LABELLE_*, but section 9 requiresLABELLE_SDL2_LIB. Reserve specific CLI-owned variables instead of the entire prefix. Define inherited-environment precedence, deterministic PATH ordering, Windows case-insensitive keys, missing/malformed env files, conflicting contributions and failure behavior. Build a fresh environment on every rebuild so removed hooks cannot leave stale values. Specify whether contributions reach replacement build processes as well as the core compile, while keeping provider host-tool compilation separate.Check the earlier toolchain consumer too: generation runs
zig build --list-stepsfor fingerprint repair beforebefore build. That can evaluate backend/toolchain configuration. This is not a demonstrated failure of the proposal, but clean-cache acceptance must exercise it; the final compile is not necessarily the first consumer of the toolchain environment.Preserve shutdown outcome semantics. The proposed test “SIGTERM, then after run still runs” is correct only when the provider handles termination and exits successfully. A clean status-zero exit runs after-run hooks; signal death, timeout and forced termination skip them. Cleanup happens in both cases. Test cancellation during provisioning, compile and after-build hooks, not only an idle server, including Windows process cleanup.
Migrate Flying Platform before publishing the removal. Recommended order: CLI 2.1 additive release -> web 0.3 and registry -> migrate and validate FP -> CLI 3.0 removal. Because FP installs latest, leaving its committed migration until after 3.0 publication creates an avoidable upgrade window. Testing an unpublished combination is useful but does not remove that window.
Recommendations on D1–D13
These are review recommendations for the owner's decision, not a claim that the draft's decisions have already been accepted.
PR boundaries and acceptance
Split A before implementation rather than after three review rounds: first context/environment/cache/defaults, then watch supervision. They have different failure modes.
Add acceptance for failed-build output preservation, changes to providers/configuration during watch, cancellation during rebuild, concurrent toolchain installation and offline cache misses. Clean HOME alone does not establish toolchain bootstrap: inherited EMSDK, PATH and installed Python can conceal missing provisioning. Include controlled external-toolchain cases and explicit missing-Python behavior.
The completion criterion is clean-machine web builds through provider-owned policy. The shrinking agnosticism allowlist is supporting evidence, not the acceptance criterion by itself.
apotema commentedon Sep 27, 2026
Thanks. The RFC body is now revision 2, and every required revision is addressed. Summary, with section references:
Watched configuration changes (§3.4).
labelle run --watch:<what>changed" and publishes nothing. That covers the provider package, version or pin, the replacement hook, the backend, the target, the output dir, the effective optimize, the replacement's settings, and the.watchcapability.project.labelleedit replans before its prebuild phase.Explicit watch capability plus publication (§3.4).
runreplacement declares.watch = true, and--watchrequires it and ≥1.3.published-<n>/and atomically swapsrun.watch.output_dir(symlink, or junction on Windows) only after a fully successful rebuild, then advancesgeneration_file. The server serves only the published dir.Environment-contribution contract (§3.2).
LABELLE_*prefix ban, soLABELLE_SDL2_LIBis allowed.before build,env_fileis also allowed onbefore generateandafter generatehooks. Contributions accumulate, so web'stoolchainhook runsbefore generateand reaches the generation-timezig build --list-stepsfingerprint pass. Package mode, which needs the fetched tree, is anafter generatehook. Clean-cache acceptance exercises the fingerprint pass in both modes.Shutdown outcome semantics (§4). These are the existing
runrules:after run;labelle run.The PR A test is rewritten accordingly. Acceptance cancels during provisioning, compile and an after-build hook, on Windows too.
FP before the removal (§6). The order is now 2.1 → web 0.3 + registry → FP committed and green on 2.1 + web 0.3 → 3.0.
Also:
EMSDK/PATH/Python, the external-EMSDKpassthrough, and the missing-Python message;The decisions are still open for the owner.
https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP
apotema commentedon Sep 27, 2026
Revision 3: watch mode removed (owner decision). The CLI will have no watch command.
cache_dir,env_filewith the full §3.2 rules, and manifesttarget_defaults.run.watchkey,.watchcapability, publication policy or session-change rules.labelle wasm serve --watchtogether with the watcher code (serve/*,pipeline/watch*), instead of keeping it as a generic core feature. cli#463 is closed as not planned.servehas no live reload.https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP
apotema commentedon Sep 27, 2026
Owner decisions: D1–D3 and D5–D13 accepted; D4 remains open
The owner approved locking D1–D3 and D5–D13 with the qualifications from the preceding review. Record these as accepted decisions in the plan:
labelle install pythonand generic managed-interpreter wiring in core because prebuild commands use it. Define that wiring for provider commands too.env_filemechanism. Before implementing, specify inherited-env precedence, deterministic PATH ordering, Windows case-insensitive keys, fresh env per rebuild, malformed/missing files and conflicts, and its scope for core versus replacement builds. Reserve specific CLI-owned variables, not allLABELLE_*; the proposed SDL use must remain possible. Verify the earlier fingerprint/configuration invocation on a clean cache.--dockerfor provider targets in this migration; retarget the CI lane to desktop. Generic container execution is a separate future capability.target_defaults. Only the target owner may declare them; reject duplicates, keep explicit CLI options highest precedence, and recompute on replan.wasmas the web provider's capability ID, without hardcoding it in core. Define collision/failure behavior; core emits one final JSON document.--emcc,LABELLE_EMSDKand.emsdk_version; useEMSDKand provider-ownedemsdk.root/emsdk.versionsettings.labelle web toolchain ...commands until projectless bootstrap is implemented separately.D4 is not approved yet. The watch capability, running-session changes, artifact publication and cancellation semantics remain to be settled. Acceptance of D3/D6 does not implicitly approve the proposed watch design.
apotema commentedon Sep 27, 2026
Owner decision: D4 approved for browser watch and live reload
The owner confirmed: “Let’s keep it for the browsers.” This supersedes revision 3’s removal of watch mode and the previous comment leaving D4 open. D1–D13 are now accepted with the previously recorded review qualifications.
Scope and responsibilities:
labelle run --watchfor browser applications through the web provider. Desktop/native watch support is outside this migration.Restore the separately scoped watch-supervision work (A2), the web-provider live-reload work, and acceptance for failed-build output preservation, configuration changes during watch, and cancellation. Carry forward the prebuild ignore/replan fix described in #463. Legacy
labelle wasm serve --watchmay still be removed as part of the command migration; browser watch remains available throughlabelle run --watch.The RFC body still describes revision 3’s removal and needs reconciliation with this decision before implementation.
fix: refuse paths that would bypass provider inputs; keep the last go…
fix: scope the --docker refusal, cap env_file cleanly, escape device …
apotema commentedon Sep 27, 2026
Owner note (2026-09-27): iOS is not needed now. Android is the only mobile platform at the moment. The §9 iOS follow-on phase is deferred, not planned. #467 keeps its small legacy-
labelle iosrefusal (added for the no-silent-bypass rule), and #468 is parked.https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP
fix: don't refuse --docker when the owner replaces build (RFC #466 PR…
feat: provider contract 1.3.0 context: cache_dir, env_file, target_de…
apotema commentedon Sep 28, 2026
Full-agnostic phase planned in #471: iOS goes to labelle-ios, backend identity (#432), target names instead of the Platform enum, and SDL moves into a provider. The allowlist ends at just the guard file. Items marked NOW run in parallel with the web phase.
https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP
feat: browser-watch supervision for labelle run --watch (RFC #466 PR …
fix: web-provider gaps for 2.1.x (provider EMSDK, managed Python for …
apotema commentedon Sep 28, 2026
Closing: the web phase shipped. #483 (PR B) removed web/wasm from the core, labelle-web 0.3.x provisions emsdk and serves browser watch, Flying Platform migrated (PR F), and labelle-cli v3.0.0 was released (#500). The §5 clean-HOME acceptance passed on Windows against 3.0.0. The remaining acceptance coverage (the web-as-
local:variant, the §8 items, and Linux/macOS clean-HOME runs) is tracked in #501.