Repository navigation
chore: pin labelle-assembler 0.105.0 - #378
Conversation
0.105.0 finishes the nested-checkout audit (assembler#692): the walks that #695's `.git`-existence probe did not reach are guarded, including two worse than the duplicate build steps the issue was reported for — `flow_scanner` DELETES what it collects, so it was deleting files out of another branch's checkout, and `scripting_splice` `@embedFile`d a parked branch's scripts into the shipped binary. Anyone using `git worktree` inside a project — a normal workflow, and the one this toolkit's own agents use — gets that fix through this pin. All five 0.105.0 release assets were checked for 200 before pinning. Claude-Session: https://claude.ai/code/session_01AM5hJ7sWCZNrKWo1xMJDEA
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (1)
Included review availability: 1 review is currently available. Your included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour. 📝 WalkthroughWalkthroughThe CLI default assembler version changes from ChangesAssembler Version
Estimated code review effort: 1 (Trivial) | ~2 minutes Merge Risk: ⚪ Minimal · up to Projects using the default assembler will use version 0.105.0, while explicit version pins and environment overrides remain unchanged. No current merge-blocking risk is identified. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
…solution (RFC #406) (#421) * feat(providers): hook manifest rules, planner and dispatch refactor (RFC #406 phase 3a) Manifest: a `replace` hook must name a target its own package declares (ReplaceRequiresOwnedTarget); `after_hooks` entries are `<package>/<id>` references, never self (InvalidHookReference); `ownsTarget`, `splitHookRef`, `qualifiedId`. Planner (`provider_hooks.zig`): `validateAll` checks the cross-provider graph once at discovery — DuplicateReplaceHook, MissingHookReference, HookReferenceMismatch, HookPhaseOrder, HookCycle — and `plan` orders each (step, target) as before → replace → after, Kahn's algorithm on the `after_hooks` edges with ties by qualified id, so the plan is independent of manifest read order. `stepOutputDir` is the step output-layout contract; `runPhase`/`finishRun` execute a phase and propagate a failing hook's exit code. Dispatch: `Provider`/`discover` are pub and `discover` validates the hook graph; `execute` is split into `resolveHost` + `runTool` over a `ToolRun` (command dispatch is a thin wrapper, its e2e unchanged); the lock read, pin check and integrity gate move into `requirePinned`; `resolveSettings` and `canonicalDir` are pub. Progress: `Reporter.beginPhaseOrStep` renames the sub-step inside the live phase, since the machine rejects a phase re-entering itself and hooks share the phase of the core step they wrap. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * feat(providers): run lifecycle hooks in the pipeline; move the .app default (RFC #406 phase 3a) Discovery runs once per pipeline invocation (only when `.plugins` is non-empty), before the reporter, the assembler or any compiler, and a failed discovery exits 1 with `provider discovery failed: <error>`. The four plans are computed up front; a project without hooks never resolves the host compiler. Hook points, each `before` → core or unique `replace` → `after`, stopping at the first failure and exiting with the hook's own code: - generate: around assembler generate + fingerprints + emsdk activation, output_dir = the generated tree; - build: around docker/host `zig build` + DLL staging, output_dir = zig-out; - bundle: around `bundle.createFromBuild`, output_dir = zig-out/bundle/<target> or the resolved --output; - run: `before` once ahead of every branch, `after` at each success exit via `finishRun` (only on a clean game exit, before the terminal `done` record; `wasm serve` runs them once the server returns). `labelle.lock` is written right after `validatePluginCoreCompat`, before generation, so a `before generate` hook already sees it. Core steps report through `beginPhaseOrStep` so a hook and the core step can share a phase. The desktop `.app` default moves to `<target_dir>/zig-out/bundle/desktop/`, the step output directory bundle hooks receive, so hooks and the core packager agree; `--output` is unchanged. Help text names the hooks doc. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * docs+test(providers): hooks doc and hermetic subprocess regression (RFC #406 phase 3a) `docs/provider-hooks.md` covers attach rules, discovery-time graph errors, ordering (after_hooks edges only, ties by qualified id), the output-layout table, the hook context, failure semantics and the limitations (no JSON relay, legacy platform handlers, bundle still macOS-only). The dispatch, contract and GitHub-pins docs point at it. `test/provider_hooks_e2e.py` drives the real CLI with a fake assembler (the plugin_compat pattern; a `.cmd` shim on Windows) and two fixture providers declared in reverse order: order regardless of declaration order, an `after_hooks` edge inverting the tie, context fields, a failing before hook's exit code with no core build and no after hook, a failing core build skipping after, ReplaceRequiresOwnedTarget / DuplicateReplaceHook / MissingHookReference / HookPhaseOrder at `labelle help` before any compiler, generate hooks seeing the lock, run hooks around the game, bundle hooks and the .app location on macOS, the refusal elsewhere. The fixture tool appends one JSON line per invocation to `<output_dir>/hooks.log` and fails on `PROVIDER_PROBE_FAIL=<hook id>`. Wired into CI next to the other provider suites on all three hosts. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * feat(providers): resolve --platform against provider-declared targets (RFC #406 phase 3b) `--platform=<t>` is a target NAME resolved by the pipeline: `desktop` is core, every other name must be declared in a pinned provider's `.targets` (`provider_targets.resolve`), and an undeclared one fails before the assembler, a compiler or the cache is touched with `labelle: no provider for target '<t>' in this project; add and pin the package that declares target '<t>'`. The `(registry: <package>)` line is added only when the cached registry document — kept by `providers resolve --accept` — lists a package whose verified cached archive declares the target (`provider_github.cachedRegistryOwner`: no network, no extraction, no package code); the registry record carries no targets, so a name is never invented. The resolved string names the target dir, is passed to the assembler and to every hook context. `project_config.Platform` stays the strict schema type (documented as the legacy codegen platform). The labelle-assembler#378 boundary is explicit: a provider target outside that enum is generated for only by its provider's `replace` hook on `generate`; without one the command stops with `... but the pinned assembler cannot generate for it yet (labelle-assembler#378)`. Schema-named provider targets reach the assembler as `--platform <name>` unchanged. Parsers: `ParsedArgs.platform_override` is a string; `parseTargetValue` checks identifier shape only (`invalid target '<t>' (targets are lowercase identifiers; run 'labelle targets')`); `astc --platform` passes the name through and rejects one without a legacy capability table. `labelle bundle --platform=<t>`: the macOS host gate moves from cli.zig into the pipeline after resolution — refusing only the core desktop packager off macOS, still before any build — the "bundling desktop instead" override is gone, and a provider target needs a `replace` hook on `bundle` (`NoBundleReplacement`), which then runs on any host into `zig-out/bundle/<t>/`. Breaking (RFC "Migration", #410): `labelle wasm serve|export`, `labelle android …` and `labelle ios …` set the same override and go through the resolver, so without the pinned provider they fail with the no-provider error; the words stay reserved. `labelle targets` lists `desktop (core)` then `<t> provided by <package>` from discovery, replacing the hardcoded backend table. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * docs+test(providers): targets doc and hermetic subprocess regression (RFC #406 phase 3b) `docs/provider-targets.md` covers the resolution rules, the registry hint, the labelle-assembler#378 boundary, `labelle bundle --platform=<t>`, `labelle targets` and the migration note (web/mobile projects add and pin their provider; `--platform=wasm` is unchanged in name). The hooks, dispatch, contract and RFC docs point at it. `test/provider_targets_e2e.py` drives the real CLI with a fake assembler that reports the target it was asked for: the no-provider error for `--platform=wasm`, the declared `.platform` and every legacy subcommand with no assembler invocation and nothing created; malformed values; the #378 message before the assembler runs; `NoBundleReplacement`; a provider replacing generate/build/bundle for `probe-target`, including `labelle bundle --platform=probe-target` running the replacement on every host with `output_dir == …/zig-out/bundle/probe-target` (and `--output`); a provider owning `wasm` making the assembler receive `--platform wasm`; and the `labelle targets` listing under a dead compiler and assembler. Wired into CI next to the hooks suite. The hooks suite's off-macOS bundle refusal now also asserts the cache stays untouched, since the gate runs after discovery. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(progress): report a compile-phase hook as a sub-step while in link An `after build` hook reports under the build's phase (`compile`), but when Zig's progress stream has already advanced the reporter to `link` that request was a backward transition the machine silently rejected: the phase stayed `link`, the detail kept the linker's name, and no record was written — JSON/status consumers never saw the advertised `hook <package>/<id>` sub-step (Codex P2 on #420). `beginPhaseOrStep(.compile, …)` while in `link` is now a detail update of the live `link` phase (never a backward move); a genuinely earlier phase is still refused so the fold cannot mask a programmer error. The reporter spec drives `compileUpdate(saw_link)` into `link` and asserts the status file record carries `phase: link` with the hook detail. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * ci: run pull_request CI for PRs based on feat/** branches A PR stacked on a feature branch (#421 on feat/provider-hooks) has that branch as its base, so `branches: [main, development]` gave it no pull_request run at all. Add 'feat/**'; `push` is unchanged. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(providers): validate the hook graph at resolve --accept before the lock `provider_github.resolve --accept` parsed every selected manifest and validated ownership, but the cross-provider hook validation only ran through `provider_dispatch.discover`, so an accept could write `labelle.providers.lock` for providers with missing references, phase-order violations, duplicate replacements or cycles, and the freshly accepted project then failed on its next help/build (Codex P2 on #420). The accepted set — remote pins plus local providers, as `discover` would see it — is now run through `provider_hooks.validateAll` after the ownership check and before the lock is written. The GitHub e2e adds a registry provider whose `after_hooks` names a hook nobody declares: `--accept` fails with `MissingHookReference` (naming both hooks) and the working lock is byte-identical afterwards. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(wasm): run the generate/build hook phases on every watched rebuild The generate/build plans were used only for the initial `wasm serve --watch` build; `WasmRebuildCtx.rebuildStaged` called the assembler, the fingerprint pass and Zig directly, so `before`, `replace` and `after` hooks were all skipped on every subsequent file change — projects whose hooks generate inputs or post-process the WASM output reloaded stale or incomplete artifacts after the first watched edit, and a `replace` hook's step silently fell back to the core operation (Codex P2 on #420). The context now carries the cold pipeline's hook site, the `generate` and `build` plans and their step output directories, and wraps the watched core steps in the same before / core-or-replace / after phases. A failing hook is a new `HookFailed` stage: reported, server kept alive, browser not reloaded — like a failing core step. The feed is already terminal during the loop, so the hooks' sub-step records are no-ops there. The serve loop is interactive, so the subprocess e2e cannot drive it; the phase runner is an injectable field (production default asserted to be `provider_hooks.runPhase`) and a unit test on the context proves the plumbing: phases run in contract order with the right output directories, a `replace` plan stands in for the core step, and a failing or unstartable hook stops the rebuild before the next stage. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(run): launch the built binary without a second warm `zig build` The desktop run path ran an unconditional `zig build` right before spawning the game, AFTER the `after build` hooks had run. Those hooks were therefore not after the final build: a hook that signs, strips or patches `zig-out/` had its work overwritten by the warm build's install steps before launch, and a failure in that second build surfaced only after publishing-style after hooks had already run (Codex P2 on #420). Decision: remove the build rather than move the hook site. The core `build` step above (`core_build`, or its `replace` hook) always runs for `labelle run`, so the second build was truly redundant — a leftover of translating `zig build run` into build-then-exec in cli#265, kept only as a "warm-cache no-op". It was not a no-op for the install steps (Zig's `updateFile` re-copies any destination whose size/mtime differ, i.e. exactly a hook-patched file), it re-ran `zig build` even after a `replace` hook had stood in for the core build, and neither `enforceCliNotStale` nor the prebuild path depended on it. Nothing built in the run branch could be newer than the core build's output, so there is nothing for a warm build to pick up. Moving the hooks around it instead would have kept a second build whose only observable effect was undoing the hooks. The `ok()` doc no longer describes the removed build's exit code path. E2E: the fixture game's build now INSTALLS `data.txt` to `zig-out/bin/` and the game prints it at launch; with `PROVIDER_PROBE_PATCH=a-post` the `after build` hook overwrites that file and the launched game must print the patched contents. The mechanism is asserted too: a manual `zig build` on the same tree afterwards DOES revert the file, so the launch proving it intact means no build ran between the hook and the game, and "build ok" appears exactly once. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(providers): discover providers after the package cache is populated On a clean checkout the pipeline discovered providers BEFORE the assembler's `install`, and `provider_dispatch.discover` treated a declared remote package with no cached `plugin.labelle` as runtime-only. The installer then fetched the provider and the build proceeded without its declared lifecycle hooks or integrity requirement, while the same command on a warm cache discovered the provider and refused it as unpinned (Codex P1 on #420). Discovery (and the hook plans) now run right after `gateThenInstall`, still before the plugin/core compatibility check, the lock write, generation and any compiler; a failure marks the progress feed `failed` ("provider discovery failed") since the reporter is live at that point. `discover` takes a `CacheState`: the pipeline passes `.populated`, under which a non-local package that is neither pinned nor in the ordinary cache is `ProviderPackageMissing` — a broken install, never runtime-only. A package DIRECTORY without a manifest stays runtime-only, because the assembler's light packs legitimately ship none (the stricter "no manifest = error" would refuse every light pack). `labelle help` and command dispatch, which run before any installer, pass `.unknown` and keep listing what exists. E2E (test/provider_hooks_e2e.py): the discovery-error cases now assert the install line IS present and the generate line is NOT (LABELLE_ZIG still points nowhere), plus the `failed` status record. New cold-cache cases with a fresh LABELLE_HOME and a remote `example/fixture-c`: (1) the installer delivers nothing → `ProviderPackageMissing` after install, before generation; (2) the installer populates the cache → the provider is discovered and its unpinned hook is refused with `RemoteProviderIntegrityRequired`, no core build; (3) a warm cache gives the identical outcome; (4) pinned via `labelle.providers.lock` + a seeded archive with a cold ordinary cache → the hook runs from the verified extraction. Never a silent skip. test/plugin_compat_e2e.py: the "absent manifest" fixture now lands the package directory at install (as the real assembler does) and omits only the manifest, which is what the case name means; an absent directory is the new fail-closed path. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * docs(providers): document the round-1 review fixes on hooks Discovery timing (after the package cache is populated, before generation), the fail-closed `ProviderPackageMissing` rule and the light-pack exception, `--accept` validating the hook graph, the single build on the run path, the `link`-phase sub-step, watched wasm rebuilds running the hook phases, and the e2e coverage list. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(providers): defer hook references into an unread package at metadata-only discovery Under CacheState.unknown (labelle help, command dispatch) a declared remote package absent from both caches was skipped on purpose, but validateAll then reported MissingHookReference for any reference into it — hiding every provider's commands from help and refusing dispatch until the package happened to enter the cache (Codex P2 on #420). discover now records such packages as unresolved and validateAll defers only references into them; a reference into an undeclared package or into a present package without the hook is still missing, and .populated still fails closed on the absence. Unit-tested at the graph and at discover against a real cache layout in both states; e2e covers help staying intact, the pipeline failing on the absence rather than the reference, and the typo still reported. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(wasm): stop the serve loop on Ctrl+C / SIGTERM so after-run hooks are reachable serveAndOpen blocked in an unconditional accept loop and the process died on Ctrl+C, so the after run provider hooks that follow it never ran (Codex P2 on #420). A signal handler (POSIX) or console control handler (Windows, SetConsoleCtrlHandler, best-effort: compile-checked only) sets an atomic flag; a waker thread pokes the listener with one loopback connection so the blocked accept returns; the loop observes the flag and returns cleanly. A repeat Ctrl+C forces the exit. The connection-based wake is the one mechanism that behaves alike on every platform (std.posix.poll is a compile error on Windows; shutdown of a listener wakes accept on Linux but not macOS). The loop's flag and the waker's poke are unit-tested; the handler itself is interactive and not driven end to end. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(hooks): run each phase on a scratch arena freed on return A wasm serve --watch session runs the generate and build hook phases on every saved edit through one long-lived Site whose allocator was the pipeline arena, so every hook's lock parse, settings, workspace paths, environment and serialised context stayed live until the server exited (Codex P2 on #420). Site gains a backing allocator; runPhase carves a scratch arena from it and frees it when the phase returns. Only the resolved host outlives a phase, on the long-lived arena; plans were already there. A counting allocator wrapper proves two phases on one site each allocate through the scratch and leave nothing live. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(run): run after-run hooks only when the game itself exited clean A zero status was not a clean exit: runZigInheritWithEnv returns 0 after the --timeout watchdog kills the game (cli#390), and the iOS/Android deploy paths return as soon as simctl launch / am start does, with the app still running — so publishing or cleanup after run hooks ran after a forced timeout or right after a mobile launch (Codex P2 on #420). runner gains runInheritTerm returning an explicit Termination (exited / timed_out); runZigInheritWithEnv keeps its 0-on-timeout contract on top of it. provider_hooks gains RunOutcome (exited_clean, exited_error, timed_out, launched_detached); finishRun runs the after hooks only on exited_clean and otherwise prints one "labelle: after-run hooks skipped: <reason>" line, with the CLI's exit status unchanged. E2E: a --timeout kill of a hanging fixture game runs no after hook and prints the skip line; a clean exit still runs them. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * docs(providers): document the round-2 review fixes on hooks Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(hooks): validate each phase group once; check pins before the compiler validateAll ordered the whole (step, target, phase) group once per hook, so N hooks in one phase ran the quadratic sort N times with every temporary left on the discovery arena. Track visited groups and order each distinct group once; a test seam counts orderPhase invocations. runPhase resolved the host compiler (running `zig version`, creating cache dirs) before requirePinned, so an unpinned remote hook on a machine without the pinned compiler reported ProviderCompilerMissing. Check every hook's pin first, as the command path does; a resolve_host seam proves the resolver is never reached for an unpinned provider. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(hooks): run before-generate hooks ahead of the ASTC and --bake pre-passes Both pre-passes read the declared PNGs, so a `before generate` hook that produces one ran too late: `generate --bake` failed on the missing file and a hook-generated PNG never got its `.astc` sibling. Move both pre-passes into the core generate step, after the before hooks; a `replace generate` hook now stands in for them too (documented). The fixture tool gains PROVIDER_PROBE_COPY so a hook can produce a generation input; the e2e drives `generate --bake` with the hook writing the declared PNG (baked, then generated) and proves the mechanism by running the same command without the hook's file (bake fails first). Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(hooks): run after-build hooks once the build's finalization is done `labelle build` finalizes the compiled tree (the Linux .desktop entry and the installable package on the platform that produces one) AFTER the `after build` hooks ran, so a signing/inspecting/publishing hook never saw the final artifact and reported success even when packaging then failed. Move the finalization inside the core build step, ahead of the after hooks; a `replace build` hook now owns it (documented). The fixture tool snapshots the step output directory in each hooks.log line; the e2e drives `build --linux-desktop` and asserts the after hooks list the entry while the before hooks do not. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * test(hooks): compare the cache before and after the off-macOS bundle refusal The targets slice added `assert not home.exists()` to the off-macOS `bundle` case, but by that point the cold-cache section has legitimately populated the cache (the pinned-provider archive, the remote package the fake install delivered), so the assertion failed on ubuntu-latest and cancelled the other hosts. The pipeline's core-desktop gate already sits before the install; what the test means is that the refusal changes nothing in the cache — so snapshot it before and compare after. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * ci(docker): declare a fixture provider that owns wasm for the Docker WASM build `--platform=wasm` now resolves only to a declared provider that owns the target (docs/provider-targets.md "Migration"), and `labelle-web` is not extracted yet, so the Docker job's scaffolded project failed on every attempt with `no provider for target 'wasm'` — the retry loop's "dependency fetch" warning was misleading. Add test/fixtures/wasm-provider, a module plugin (the assembler wires every declared package as `b.dependency("labelle_<name>").module("labelle_<name>")`) whose v2 manifest owns `wasm` and nothing else, and splice it into the scaffold's `.plugins` before the build; the grep on the empty `.plugins` makes a template change fail loudly here rather than as a no-provider error. Verified locally against the assembler at origin/main: `labelle build --platform=wasm` resolves the target through the fixture, the assembler receives `--platform wasm`, the lock pins `wasm_fixture`, the plugin is copied into `.labelle/deps/labelle-wasm_fixture` — inside the tree the container mounts, like core/engine/gfx — and the emcc link produces `zig-out/web/game.wasm`. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(targets): reject a Windows reserved device name as a target A provider could declare `con`, `nul`, `com1`… as a target: identifier- shaped, so every check passed, but the default bundle directory `zig-out/bundle/<t>/` (and `.labelle/<backend>_<t>/`) cannot be created on Windows, so `labelle bundle --platform=<t>` failed after generation and build unless `--output` was given (Codex on #421). Move the device-name rule from the archive validation in provider_github into provider_contract (`windowsReservedDeviceName`), define `contract.targetName` = identifier and not a device name, and apply it at every entry: the manifest's `targets` and each hook's target (`ReservedDeviceTarget`, at validation), `args.parseTargetValue` with its own reason in the diagnostic, and `provider_targets.provisional`. Unit tests for the rule, the manifest, the parser and near misses (`console`, `nul0`, `com10`, `lpt`). Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(providers): scan the registry hint on a scratch arena, bounded `cachedOwner` read every cached release's archive on the caller's arena — the pipeline's — so a 128 MiB archive plus up to 512 MiB of unpacked tar per inspected release stayed live until the command exited, and a registry with several cached releases could exhaust memory for a diagnostic (Codex on #421). Each release now decompresses on its own scratch arena, freed before the next, and the scan stops at the first owner or after `registry_hint_scan_limit` (16) cached releases; an uncached release costs one failed open and is not counted. `cachedManifest` reports a missing archive as an error so the scan can tell "nothing read" from "read, no owner". Unit test with limit+1 cached decoys ahead of the owner. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(targets): refuse an unpinned remote package as a target owner `provider_dispatch.discover` falls back to the ordinary package cache for a remote plugin with no `labelle.providers.lock` pin and returns it with `verified = false`; the resolver accepted its target declaration anyway. For a schema-named target such as `wasm` with no hooks nothing ever called `requirePinned`, so generation and build proceeded on an unverified cached manifest, past the provider integrity boundary (Codex P1 on #421). `resolve` now returns `UnverifiedTargetOwner` when the owner is unverified — owning a target means generating, building or bundling for it, the same boundary as a hook that runs — and the pipeline reports it naming the package and the `providers resolve` / `--accept` way out. `labelle targets` still lists the declaration, marked `(unpinned)`. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(targets): decide target ownership before any side effect when it can be With `.plugins` present, an identifier-shaped typo (`--platform=waasm`) reached the ownership check only after `.prebuild`, the assembler resolution, the ASTC prepass and `install` had run (Codex on #421). Restore an early verdict: for a provider target the pipeline first runs the same metadata-only discovery `labelle targets` does (`.unknown`: cached manifests, no installer). `discoverAll` now also reports the declared remote packages it could not read; when that list is empty every declared package was readable and the verdict — no owner, or an unpinned owner — is the one the post-install check would reach, so it lands here with the same diagnostics and nothing run. A manifest that fails discovery fails closed here too: the install cannot mend a manifest it can already read. Only a partial view (cold cache, no pin) defers to the post-install discovery, which stays the authoritative check with its `failed` record. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(astc): skip the prepass for a provider target with no capability table A provider target outside the `Platform` enum derives `parsed.platform = .desktop` for the legacy sites, so with desktop atlases set to ASTC the prepass ran `cmdAstc --platform desktop` for e.g. `probe-target`, encoding siblings by desktop capabilities for a provider's own `generate` to pick up and bypassing `cmdAstc`'s rejection of such a name (Codex on #421). Key the prepass on `provisional.legacy`: only `desktop` and the schema-named provider targets have a table; a provider owns its target's asset pipeline. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * test(targets): cover the round-1 fixes end to end, and document them The targets e2e now checks: a local provider's non-ownership refused before anything runs; an identifier-shaped typo leaving a `.prebuild` marker step unrun while a resolving target runs it; a remote package on a cold cache deferring the verdict to after the install (the `failed` record stays the authoritative path) and, unpinned, refused as an owner both after the install and — warm — before it, with the cache untouched; the `(unpinned)` mark in `labelle targets`; and the ASTC prepass running for `desktop` but not for `probe-target`. The fake assembler's `install` can populate the cache (the hooks-suite pattern). docs/provider-targets.md describes the early verdict, the pinned-owner rule, the reserved-device-name rule, the bounded registry-hint scan and the Docker fixture; the contract's name rule gains the device-name clause. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(hooks): keep the run hooks on wasm serve|export --no-build The --no-build branch returned before provider discovery, so every declared before/replace/after run hook was silently skipped although serving or exporting the existing artifact is the same run step the building path hooks. Discover (metadata-only, nothing is installed on this path) and run the run plan around the export or the serve loop, skipping only generate and build. E2E: `wasm export --no-build` over a pre-made web dir runs the wasm run hooks around the export with no install/generate/build, and a failing before hook stops the export with the hook's exit code. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(hooks): replan the provider hooks on every watched rebuild `wasm serve --watch` captured the hook plans and the site's config once at startup and reused them for every rebuild, so a watched edit to project.labelle, a provider manifest or provider_config never reached the lifecycle: removed hooks kept running and added ones were skipped until the server restarted. WasmRebuildCtx gains a per-rebuild `replan` seam, run after the prebuild steps and before any phase; production installs WatchReplan, which re-reads the project, rediscovers with the cache populated and replans both phases on a fresh arena, installing the result only on success (a failed replan stops that rebuild and keeps the last good plans). Unit tests cover the seam (invoked once per rebuild, the fresh plans are what run, failure stops before any phase) and the production replan against a real temp project (manifest edits, a broken manifest, a dropped plugin). Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * test(hooks): e2e for the pin-before-compiler order; document rounds 3 fixes fixture-c gains a `before generate` hook so the cold-cache unpinned build refuses it before any compiler: with LABELLE_ZIG pointing nowhere the error is RemoteProviderIntegrityRequired, never ProviderCompilerMissing, and nothing is generated; once pinned, the same dead compiler is the failure (the mechanism). docs/provider-hooks.md covers the once-per-group validation, the pin order and the new e2e/unit coverage. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(targets): bound manifest-less hint reads; name the unpinned refusal in status Round-2 Codex findings on #421: - provider_github.cachedOwner: a cached archive that verifies but carries no `plugin.labelle` was read, verified and decompressed whole yet not counted against `registry_hint_scan_limit`, so a registry of such releases could read past the bound. Count every archive read; unit test seeds `limit` manifest-less releases ahead of the owner and shows the owner is not read (and is, one release fewer). - pipeline.confirmTarget: an unverified (unpinned) remote owner returned the same null as an absent owner, so build/run/bundle persisted `detail: "no provider for target"` for both. The refusal kind now survives to the `failed` progress record as `"unpinned provider for target"`; the e2e reads both records. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(bundle): find the host-built game as <name>.exe on Windows resolveBuiltExe looked for '<name>' and 'game' in zig-out/bin, but Zig installs '<name>.exe' on a Windows host, so 'build --linux-desktop' (and bundle) failed with BinaryNotFound there. Append the host exe suffix to the declared name and both fallbacks. Fixes the Windows CI job on #420. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(bundle): keep the resolved exe name bare; only the path carries .exe 0004dc8 put the suffix into ResolvedExe.name, so the Linux desktop entry became 'game.exe.desktop' on Windows. The name stays '<name>'. Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * style(bundle): keep ResolvedExe's doc comment on the struct Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP * fix(targets): revalidate the watched target, scratch the early discovery, hand bundle hooks --build-number - WatchReplan.run confirms ctx.hooks.target against the rediscovered providers before installing anything: an edit that drops the owning package or unpins a remote owner fails that rebuild with NoProviderForTarget / UnverifiedTargetOwner (and the cold pipeline's diagnostic) and keeps the previous generation (Codex P1 on #421). - The pre-install ownership check runs in earlyTargetCheck on a scratch arena freed before it returns; only the verdict leaves, so pinned archives are no longer held twice or for a --no-build server's lifetime (Codex P2 on #421). Tested by counting the backing allocator. - LABELLE_CONTEXT gains an optional build_number, set only for bundle-step hooks when `bundle --build-number` was given and omitted (not null) otherwise; documented in provider-contract-v1.md and provider-hooks.md; e2e asserts the bundle replacement's captured context carries it and the build hook's does not (Codex P2 on #421). Claude-Session: https://claude.ai/code/session_01R2K5sJDL5Msn1uRTkybiDP
Routine pin bump, same shape as #374 and #377.
0.105.0 finishes the nested-checkout audit (assembler#692). #695's
.git-existence probe covered the scanner; 0.105.0 covers the walks it didn't reach — and two are worse than the duplicate build steps the issue was filed for:flow_scannerdeletes what it collects, so the gap meant deleting files out of another branch's checkout;scripting_splice/scripting_transpile@embedFiled a parked branch's scripts into the shipped binary.This bump is what carries that to projects with no
assembler_versionpin and noLABELLE_ASSEMBLERoverride. Relevant to anyone usinggit worktreeinside a project — including this toolkit's own agent workflow.Verified before pinning: all five 0.105.0 assets return 200.
Local:
6/6 steps succeeded; 786/811 tests passed (25 skipped), zero compile errors, zero failures — identical tomain.Known gap still open upstream, tracked as assembler#717: native staging live-links the whole script dir, so a nested checkout inside
scripts/can still reach rustc. Not a regression, and fixing it trades away the live edit loop — its own decision.https://claude.ai/code/session_01AM5hJ7sWCZNrKWo1xMJDEA
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.