Working state — a note taken while the work happens, not a specification. The system as it is meant to be is in Architecture.

Carried

Per work-proposal.md §5: items carried out of the retired plans, plus defects logged during the programs-arc integration, so nothing silently drops. One line each. This file is a ledger, not a plan — an item leaves it by being fixed or by being promoted into a lane.

Carried out of the retired plans

  • hpack miscompile — the extended suite’s known compiler defect; reported during the 2026-07-27 test-all triage, never root-caused.

  • E3 scratch-image check — the PLAN_IMAGE_MAX=256 daemon-lifetime scratch-image cap needs an honest named refusal instead of a wedge.

  • Platform rollout — the remaining per-OS provider folders beyond the proven darwin/linux/windows set.

  • Dangling jump_false in plans/archive/qop.cm fn 56 — a real emitted branch with no target; parked because the file is archived evidence.

  • make local lacks daemon-fresh — plain make can leave a daemon serving the previous binary; only make smoke and make seed restart it.

  • Foreign-daemon-socket trap — with no .pit/ of its own, a worktree’s ./pit binds the socket owner’s shop, so make builds into that worktree. Run plain make first in every fresh worktree.

  • A make can wedge with no children and no CPU, and killing it strands a pit forge on the worktree socket. Hit during the D2 integration (2026-08-06, arc/integrate4). make -j8 in a fresh worktree ran far enough to produce ./pit and .pit/, then sat for 12 minutes in state S with 0:00.03 CPU, zero children and no submake — nothing was building and nothing was going to. Killing it left a pit forge darwin --out build --boot boot/root.cart reparented to init and still holding .pit/pit.sock, so the next make refused with

    pit: shop daemon has different build signature
    pit:   daemon=cold-20260806005034  current=forge-host-nan64-v1
    pit: preserving endpoint because its process is not confirmed dead
    

    which is the runtime being RIGHT — it will not serve a stale binary — not a second defect. The recovery is the same as the foreign-socket entry above: find the socket’s owner (lsof -t .pit/pit.sock, or read .pit/pit.pid), CHECK THE PROCESS PATH IS YOUR OWN WORKTREE before touching it (several lanes share this machine), kill that one process, remove .pit/pit.sock and .pit/pit.pid, then re-run make — which went clean first try. Whether the original wedge is another face of the warm-start dropped-continuation cluster below is UNDETERMINED. It has the family resemblance — an idle process waiting on an answer that never arrives — but nothing was captured that ties it to that path: the hang was in make itself rather than in a ./pit client, no observe.jsonl row was collected before the kill, and it has not been reproduced. Recorded so the next sighting has something to match against, not as a diagnosis.

Carried out of the pool-lane press landing (arc/pool-lane)

  • F4 (delete the static link lane) DID NOT RUN, and the reason is a live consumer, not caution. plans/proposal-notes/deletion-inventory.md §5.1 names the static lane’s two keepers: the press (R6 flips it — built and measured here, NOT landed, see below) and the DEV REALIZE LANE (R7 moves it — not done). The second is still there and is the whole dev loop: pit-shop/clerk.ce:1132 realizes every actor start with no link in the request, shop_realize.cm:1068 therefore leaves build_msg.link unset, and shop_build.cm’s link_mode falls through to null. The seeded QOP universe (seed_fleet_entry’s FIRST realization) is a second consumer, for the same reason: a dev daemon adopts those units and lowers them per module, and one pool per unit cannot resolve a finalized LOAD_MODULE_RESULT (§5.4). F4 unblocks when R7’s granularity work moves the dev lane onto pools, and not before. Everything §5.2 lists is still correct as a list; it is the precondition that is unmet.
  • static_bindings.canonical is NOT static-lane residue, and §5.2 is wrong about it. The inventory lists static_bindings on the executable module row among the deletions. Its exports sub-map and the cart NAMES/IMPORTS rows are residue; canonical is now load-bearing for the press: it is the only request→provider map that covers NATIVE providers. The manifest’s bindings[] rows keep an edge only when the provider is itself a manifest UNIT (executable_manifest.cm:329-334), so use('blob') — every extension — has no row there, while build_exec_plan gives it an ordinary ENTRY_NATIVE plan slot. Covering the press from the manifest alone refuses on the first blob import. Whoever runs F4 must keep canonical or give the manifest a native binding row first.
  • THE PRESS IS NOT ON THE POOL LANE — FIXED and landed (arc/function-ref). shop_tools/cement.ce now passes link: "pool"; the pressed mcode product runs in a bare directory and writes the correct 13,763-byte output. Census 88 finalized sites on the 50-unit product (0 constant + 88 function, 446 generic) against 0 for every press before it; cart 3,317,776 → 3,321,872 B (+4,096, +0.12% — finalization is currently a small size COST, because inline_budget: "none" leaves the call-directness payoff uncollected; that is CP2’s half, below). The fault was NOT source/mach_pool.c:4590. The function_ref lowering is correct; the failure reproduced with finalization forced to zero sites, so the variable was the pool-lane compile, not the finalized edge. Root cause: mcode_link.rewrite_function read ["get", d, s, 1] in any function as naming the module main’s frame. Depth 1 is the main only for a DIRECT CHILD, and pit-compiler/mcode.cm is one outer function holding the whole compiler, so its inner functions had their own parent-frame slot numbers looked up in the main’s origin map and were handed record link facts for slots holding numbers (gen_function got two). The linked streamline then folded is_num to false and the guard’s outlined panic became unconditional — hence <compiler panic: operands must be numbers> at gen_function. Fixed by main_lexical_depths (a get resolves to the main only at the reading function’s own depth, which also RECOVERS every legal site in a deeper function: 62 → 88 finalized, and with them 285 → 446 recognized-but-generic members) plus a retraction rule on link_types, which is keyed by slot and applied by streamline at every import/member_import/get writing that slot. Pinned by tests/mcode_link.cm::test_import_origin_follows_lexical_depth_to_the_main and by tests/pool_link_run.ce, which presses a multi-unit program with a finalized member into one pool and RUNS it — the gap the fuzzer’s linked_unlinked dimension cannot reach, because it links single-unit programs with no imports.
  • THE BOOT PRESS IS A SECOND REMAINDER, with its own unnamed failure. Even with the run-time defect above fixed, the boot press needs more: it was wired the same way — shop_build.prepare_boot_executable taking link: "pool", and a SECOND pool-lane realization inside seed_fleet_entry beside the static one the QOP universe needs (the dev daemon ADOPTS those units and lowers them per module, and one pool per unit cannot resolve a finalized LOAD_MODULE_RESULT) — and measured over three seeds. Every time: the root executable presses correctly (28 units, 24 finalized sites, 0 constant + 24 function, 219 generic, one pool section), and then bootstrap answers true, exits 1, writes no artifact into --out, and presses no resident. No refusal, no disruption, no stack; the only clue is that the reply is the bare value true where bootstrap_artifacts returns {ok: true, …}. Reproduce directly against a prepared candidate shop, which takes seconds warm: env -i PATH=$PATH HOME=$HOME PIT_INPROCESS=1 PIT_CONTENT=<press-1/boot.qop> build/forge-press/clean/pit --shop build/forge-press/shop-2 --content <press-1/boot.qop> bootstrap --out /tmp/x The changes are in this branch’s history (reverted in pit-shop/shop_build.cm with the reasoning kept at both sites); re-applying them is two edits.
  • Cross-unit body inlining became live and is deliberately off at the press. mcode_link.import_cross_unit_bodies fires only where edges were finalized, so it went from never running to running on every press the moment the press took the pool lane. boot_cart.BASE_LINK_PLAN names inline_budget: "none" so this landing’s artifact delta is attributable to finalization alone — the same call the press switch made about stripping. It is one word plus a peak-memory and artifact measurement to turn on, and it is CP2’s half.
  • The clerk reaching its 8 GB heap cap during a press is NORMAL, and that cost an hour. gc pit-shop/clerk: died, peak heap 8388608KB, final 8388608KB held / 3068771KB live appears in the log of a press that SUCCEEDED (the forge’s press-1) as well as one that failed. It is the actor’s teardown report, not a failure. Anyone reading a candidate shop’s observe.jsonl for a cause should discount it.
  • mcode_lower: unknown label should be a REFUSAL and still is not. source/mach_pool.c’s jump-patch loop warns on a label it cannot resolve and lowers the jump anyway, so a producer that drops a labelled block while keeping a jump to it ships a branch to an arbitrary pc. One producer was found and fixed here (pit-compiler/panic_outline.cmfind_call / semantic_after walked across a LABEL, so replace_sites spliced a jump target out; measured on archive/qop.cm’s archive_from_entries, which was IN THE SHIPPED BOOT CART with a jump to store_bad_key_2544, a label that was not there. Reproduced on BOTH link lanes, unlinked, so it predates every press change). Turning the warning into ml_set_error(...); goto fail; was written, proved to name the defect exactly, and then reverted, because it is a two-generation crossing: the forge’s first press runs the committed PREDECESSOR’s compiler, so the refusal fails that press on defects the fixed compiler no longer emits and the seed can never reach the generation that would satisfy it. Flip it in the generation after this one, and read what it says — tests/compile.cm lowered by the pre-fix compiler produced a second dangling label (store_field_stone_1195 in run_linked_consumer) that the fixed compiler does not, but whether any producer remains is precisely what only the refusal can answer.
  • The press pays a second realization of its closure, per seed. seed_fleet_entry now realizes each fleet member twice — static for the QOP universe, pool for the cart resident. That is the honest cost of two genuinely different artifacts while the dev lane is static, and it is the thing R7 removes: one lane, one realization.

Defects logged during this integration

  • A START PLAN CANNOT BE FROZEN OR STEPPED — only interrupted. Residual of arc/preempt-loops (2026-08-06). A module body and an actor’s top-level body run through C shims (plan_c_run_unit -> pit_plan_run_window -> a fresh dispatch loop), so they execute at vm_call_depth >= 2, where no poll site can park: a park saves a frame and a pc and cannot unwind the host frames in between. Killability was closed (TIMER_START_KILL plus the kill timer’s escalation to pause flag 2), and that was the part that mattered — but a debugger still cannot stop a loop in a module body and read it, and pit debug reports such an actor as “between turns” because it genuinely has no turn of its own: the work is on its starter’s. The fix shape is the one already used for image callees (source/mach_pool_test.c inloop_preemption_test): enter a plan’s units IN-LOOP instead of through a C function value, which keeps depth at 0 and makes the whole start plan an ordinary freezable stretch of Pit. That belongs to whoever next opens source/start_plan.c.

  • ctx->vm_call_depth is zeroed only at context creation. Nothing resets it per turn, so one leaked decrement on any disrupt path would leave that actor permanently unfreezable and un-steppable — silently, with every suite still green, because no test asserts the depth is 0 at a turn boundary. No leak was found; nothing pins it either. Noticed during arc/preempt-loops.

  • THE SECOND pit compile OF A DAEMON SESSION NEVER STARTS ITS ACTOR. Diagnosed 2026-08-05 (arc/runaway). This subsumes both the “cache-hit pit compile linked_plan hang” and the “pit compile profile wedges on second invocation” sightings: they are one defect, and it is NOT the scratch-image cap and NOT a memory event. What was measured, on a fresh daemon each time and reproducible every run:

    • ./pit compile parse runtime/tests/fixtures/lto_mod.cm — run 1 answers in <1 s (cache hit); runs 2..5 never return (killed at 60–90 s).
    • ./pit bundle info boot.qop run 3× in one session: all three answer. So it is shop_tools/compile.ce specifically, not “any second start”.
    • During the hang the DAEMON IS IDLE, not thrashing: sample <daemon> shows every actor_runner thread parked in _pthread_cond_wait, 0% CPU, 78 MB RSS. pit ps, pit why, pit bundle all keep answering normally in the same session, so the listener and the clerk are alive.
    • The CLIENT is parked in pit_daemon_launch -> pit_daemon_start -> poll (platform/posix-daemon/source/daemon_posix.c:481) waiting for an exit line that never comes — it connected and sent its open frame.
    • .pit/log/observe.jsonl for run 2 shows a pit-shop/terminal actor start and couple, then nothing at all for the whole timeout, and NO actor_start for compile. Run 1’s compile actor starts and dies normally 30 ms in. So the start request is accepted and then dropped: some requestor on the warm start path for this program is never answered. Nothing logs, on any channel. FIXED 2026-08-06 (arc/warm-start). Root cause: shop_realize.cm’s hydrate_cached_executable walked the executable’s module list by RECURSION — next -> hydrate_mach -> build_read(cb) -> hydrate_mcode -> build_read(cb) -> next — and build_read is fully synchronous (shop_store.cm:503 reads the object and calls back from its own frame). So the walk was never a chain of turns: it was one call stack growing ~10 live frames per module and unwinding only at the last one. shop_tools/compile.ce has 79 modules (~800 frames), and a WARM start reaches the hydrator from the deep end of the cache-hit chain (index load, per-dependency validation through parallel over 98 deps, entry decode) — the two together crossed ACTOR_STACK_LIMIT (1024, source/pit_internal.h:2218) partway down the list. The raise killed the clerk’s TURN with the shop’s realize_start requestor unresolved: no actor, no reply, no log, clerk still alive. Instrumented proof: the last thing the clerk emitted was hyd next i=63 of 79 modules; the same hydration from the cold-build path completed all 79. The “only compile” specificity was module COUNT — bundle is small enough to fit. Fix: the module walk is now a pump, so re-entering next while a pump is running asks for another iteration instead of nesting one; depth is constant in the module count whether the reads are synchronous or not. Pinned by tests/warm_start_repeat.ce (baseline suite). E3’s PLAN_IMAGE_MAX was never it, as recorded.
  • pit cement --app sat at 0% CPU for 19 min — SAME DEFECT, closed by the same fix: cement starts several large programs in one session and every one after the first took the warm path into the recursive hydrator.

  • shop_build.cm cache_executable_mach_blobs was the same shape — FIXED 2026-08-06, same arc. Found while fixing the above: it walked the same module list with the same next -> build_write(cb) -> next recursion, and build_write is synchronous too, so its depth was also linear in the module count (~6 live frames each, ~475 for a 79-module executable). It had never been seen to fail because it runs on the COLD build path, which reaches it from a much shallower stack than the cache-hit chain that broke the hydrator — but that was a margin, not an invariant, and it shrinks every time an executable grows or a deeper caller appears (products and one-pool pressing are adding callers now). It carries the same pump verbatim. These are the only two instances of the shape that were taken; a third, if one is found, belongs in this ledger rather than in a broad refactor.

  • The clerk decodes a whole executable into its own heap in one turn. shop_build.cm:474 embed_executable_cached_payloads walks every module of a realization and, for each, reads the mach blob and DECODES the mcode artifact into live pit values on the clerk’s heap, synchronously, in one turn. It holds all of them at once — nothing streams and nothing is released between modules. That is why the clerk reaches ~1 GiB on the heaviest programs (the wave-2a seed press pinned it at 1073741816 of 1073741824 in exactly this frame). It is a memory-VOLUME defect proportional to program size, not a leak and not a stale-key memo: there is no cache in this function, and the one session memo in the shop (shop_fetch.cm closure_memo) stores small resolution graphs and is bounded at 4096 with a whole-map clear. Fix shape: hydrate lazily or in bounded batches, or hand the decode to a builder worker whose heap is freed wholesale. B5 measurement (2026-08-06): it is now pinned at the RAISED ceiling. Every long-lived clerk in a three-press make seed reports heap_peak_bytes exactly 8589934592 — B4’s 8 GiB ACTOR_MEMORY_LIMIT — in all three presses, including the one driven by the pre-B5 count-clamped clerk, so the saturation is this defect and not the fleet’s width. Two consequences for whoever fixes it: the number is SATURATED, so no measurement can show an improvement until it drops below 8 GiB; and while it sits at its ceiling the clerk’s allocations are refused by name inside catalog_put_value’s disruption arm, which surfaces as the bare, causeless mach cache write failed (observed once; shop_build.cm:982 — the arm swallows the refusal, so the message names neither the limit nor the actor, and hardening it to say which would be cheap). NOTE for whoever picks this up: it is NOT the same defect as the member_memo_key crash below, despite both frames being in the clerk — see that entry for the evidence separating them.

  • FIXED (arc/runaway, 2026-08-05) — publish: target stopped, the member_memo_key clerk death. It was a null concatenation, not a memory runaway. shop_fetch.cm member_memo_key (added by dd4026860, the session closure memo) spelled its key with text(owner_pkg). owner_pkg is NULL for a member that belongs to no package: package_from_locator cannot attribute an absolute host path (shop_source.cm:107 — the first /-split part is ""), and the only fallback is the request’s package, which a publish does not carry. text(null) is null, so the concat raised cannot apply '+': tried to add text to null — inside the file() callback, where the scan’s disruption arm cannot see it and parallel swallows a requestor raise. The clerk died mid-walk; callers saw target stopped; and because the clerk is not respawned, the daemon then answered target is gone to every later command until ./pit down, which is what made it look like a wedge. Evidence it was never a memory event: the clerk’s own actor_death record for the failing run reads peak heap 32768KB, final 16384KB held / 2042KB live — 32 MB, three orders off a gigabyte — and a panic record on the same millisecond names the add. A minimal probe (walk a closure rooted at a packageless absolute path) reproduces it with a one-line stack at member_memo_key and passes with the fix. Regression test: tests/store_freshness.ce packageless_closure_checks. One sibling of the same shape is still in the file and was deliberately left alone for want of evidence: shop_fetch.cm:387 spells "missing executable '" + text(item.target), which would raise the identical add text to null on the path whose whole job is to REPORT a failure. Harden it next time that function is opened.

  • FIXED (wave-2b integration, 2026-08-06) — publish walked the compile-only metadata key and refused its own closure. F3 makes mcode the only shipped payload, which takes the publish closure walk down the mcode arm by default for the first time. The walk pushed mcode_unit_metadata_key(hash) unconditionally, but a unit’s compact metadata is filed under ONE OF TWO derived keys (shop_store.cm:129-182): a completed import scan takes the “complete” slot, a compile-only record takes the plain one, so the two never contend for a last-writer-wins catalog row. Every fully scanned unit has no plain-key row, so publish reported closure incomplete for …: 6 missing {mcode_metadata blake2:e71b82a8… (module lang-endowments/log) <- …} and cli/{bundle_info,ship_app_bundle} went red on the merged tip while passing on arc/runaway alone. The walk now asks the same question shop_store.mcode_unit_metadata asks — complete key first, plain key as the fallback — so the bundle carries the row the receiving reader will look under, and a unit with no metadata at all is still reported missing by name. The lesson generalises: a latent bug in a default-off arm becomes a landing bug the moment a deletion lane flips that default, and per-lane gates cannot see it because neither lane is wrong on its own.

  • F3 follow-up not landed: --payload source is advertised but neither implemented nor refused. shop_tools/publish.ce:19 offers “mcode (default) | source” and the retirement message at shop_realize.cm:1931 points callers at source, but realize_payload_mode (shop_realize.cm:332-334) accepts only mcode/mach/mixed, so --payload source silently falls through to an mcode publish. Either build the source bundle or refuse the word by name; a flag that quietly does something else is the shape F3 spent the rest of its landing deleting.

  • shop_tools/mcp.ce does not compile in-processcompiler.mcode_unit_result returns null for it in tools/builder_diet_probe.ce, and ./pit compile mcode shop_tools/mcp.ce answers file not found for a file that is on disk. PRE-EXISTING and not a merge effect: identical failure at arc/builder-diet’s own tip and at the merged tip, in the same probe run. Found because wave 2b ran B3’s byte-identity harness over a 217-unit superset of B3’s own 144-unit corpus; B3’s corpus excluded it. Probably a locator-resolution gap rather than a compiler defect, but nobody has looked.

  • FIXED 2026-08-06 (arc/warm-start, merged in this batch) — a SECOND pit product / pit cement in one daemon session never answers; it was the FOURTH member of the “second X per daemon session never starts” cluster, and the whole cluster is one defect. The root cause is the recursive module walk in shop_realize.cm hydrate_cached_executable overflowing ACTOR_STACK_LIMIT — see the FIXED entry at the top of this section for the full diagnosis. My guess about the shared shape was wrong and is recorded here as a caution: I wrote that the four sightings had “imports pit-shop::shop_build into a CLI actor” in common. They do, but that is a correlation, not the mechanism — what they actually share is being big enough that the hydrator’s per-module frame cost runs out the stack. A shared import is a cheap thing to notice and a bad thing to reason from. The original report follows unchanged, because its measurements are what made the cluster visible: Measured repeatedly during D2 (arc/product). The first invocation completes normally (cart written, report printed); the second produces no daemon output at all — not even a fetch: line — and the client waits forever. ./pit down between runs is the workaround, and it is why tests/cli/product.ce makes exactly one pit product call. Same signature as the pit compile entry above — start request accepted, nothing logged on any channel, idle daemon, client parked — so treat it as the SAME defect and one investigation, not four: the likely cause is the dropped continuation on the warm-start path that entry names (shop_realize.cm find_realization / load_cached_executable, shop_build.cm:474 embed_executable_cached_payloads), and it is NOT the E3 scratch-image cap. The four sightings are pit compile parse, pit compile profile/linked_plan, pit cement --app, and now pit product/pit cement --product; every one of them imports pit-shop::shop_build into a CLI actor, which is the shared shape. Fixing it once should close all four. It makes pit product a once-per-daemon command today — survivable for a Makefile line, not for a loop.

  • OPEN, AND IT CONTRADICTS THE CLUSTER CLOSURE ABOVE: pit compile profile STILL WEDGES WITH THE WARM-START FIX MERGED. Measured 2026-08-06 on arc/integrate4 at 967cac5d8 — i.e. with arc/warm-start’s hydrator pump in, and with its own tests/warm_start_repeat.ce PASSING in the same gate run. pit --quiet --machine compile profile pit-compiler/fold.cm, the bench.stages lane of perf/run_bench.py, ran 9 h 09 m without answering and had to be killed. arc/warm-start was reported as closing the whole “second X per daemon session” cluster including this exact command; it did not close this one. So either there is a SECOND, distinct hang on that path, or the hydrator pump is incomplete. Do not assume the fix covers a sighting merely because the sighting was filed under the cluster — that assumption has now been made once and been wrong. Consequence for gates: make bench cannot complete; perf/run_bench.py --skip-stages gets the other two lanes. Not investigated — recorded on sight, budget spent landing the merge instead. NOT REPRODUCIBLE AT dev @ 32f03fdc4 (arc/fix-hangs, 2026-08-07), AND NOT FIXED BY ANYTHING IN THAT BRANCH — the acceptance test simply passes. What was run, on the tip 42 commits past the sighting: the exact bench invocation (--quiet --machine compile profile pit-compiler/fold.cm --repeat 1) THREE times in one daemon session — 22.6 s cold, then 5.1 s and 5.2 s warm — and the full perf/run_bench.py with NO --skip-stages, which completed all three lanes and reported 273 metrics … ok=True, failed: 0. So make bench completes and the stages.* lane can be baselined again. The cause was NOT established and this entry should not be read as a diagnosis. The sighting was never instrumented (recorded on sight), so there is nothing to match against; what can be said is which candidate explanations the range admits. The leading one is the clerk’s one-turn decode — the wave-2a wedge recorded further down, where the clerk sits in embed_executable_cached_payloads → decode_artifact at the 1 GiB ceiling paying a ~365 ms full collection per 32-byte allocation for 0% recovery. That is a process that is busy and making no progress, which is the shape a 9-hour no-answer takes, and both fixes for it — arc/clerk-decode (e33b1c850) and the streamed press (837153244) — were verified ABSENT at 967cac5d8 and are present at 32f03fdc4. Unproven, because reproducing at the old tip needs a cold build of that tree plus a run that may take another nine hours. What was positively RULED OUT is the shared-cause story with the pit debug sites leak below: a leaked immortal node makes the next client REFUSE BY NAME in 12 ms (pit: daemon process is alive but its endpoint is absent; refusing a second daemon), not hang, and that refusal (01bba7317, 2026-08-02) already predates the sighting. The two are separate. If it recurs, instrument before killing: sample <daemon> answers the busy-vs-idle question that decides between the clerk thrash and a dropped continuation, and it is the one datum the original sighting lacks.

  • FIXED 2026-08-07 (arc/fix-hangs) — tests/cli/debug_tool.ce leaked one immortal node per run, and the recorded mechanism for the orphan class was WRONG. The survivor is not “a completed client parked in teardown on a pthread_join in exit_handler” (plans/todo.md’s entry, now corrected): lldb on a live survivor showed thread #1 in main_backend_run → CFRunLoopRun — it had never entered teardown — with scheduler_is_shutting_down() == 0, pit_is_daemonized() == 1, and scheduler_actor_count() == 0. Zero actors, still running. It held no socket at all (a healthy node holds a unix socket, a TCP listener and two kqueues), because the listener is an actor and died with the rest. Unreachable, immortal, silent on every channel; pit down printed “pit: node stopping” and the node did not stop. Root cause: the engine’s “am I done?” question was asked in exactly ONE place — the tail of actor_free, the actor destructor (source/scheduler.c, if (was_registered && scheduler_actor_count() == 0) scheduler_shutdown();). But the live count drops earlier and elsewhere, in actor_halt’s shdel(actors, actor->id), and actor_free is not guaranteed to run: a halt that lands while the actor is inside a long turn deregisters it and can leave no reaper that ever reaches the destructor. Instrumented proof, one line apart in the two traces — a clean run ends HALT … debug_tool_worker count=0 / FREE was_reg=1 count=0 (shutdown fires); the leaking run ends HALT … debug_tool_worker count=0 with no FREE at all. Fix: scheduler_note_actor_gone() asks the question wherever an actor leaves the registry — actor_halt as well as actor_free, scheduler_shutdown being idempotent — and counts starting_actors alongside actors so the gap between the last actor leaving and the next registering cannot trigger a premature exit. Reproducer: start a detached long-turn actor, wait ~1 s so down lands mid-turn, down. Before: leaked every time, and down reported timed out waiting for node shutdown (socket or captured process N still alive) — the client-side refusal was already honest; the node was the silent one. After: 5/5 clean exits, and cli/debug_tool runs 3× with ZERO survivors (previously one per run). Pinned by tests/cli/daemon_exit.ce (added to CLI_TESTS). This also delivers half of the deferred ruling “a daemon whose listener halts should exit” in plans/todo.md — the zero-actor case now exits; a listener that dies while OTHER actors live still leaves an unreachable node, and that remainder is still a ruling. cli/debug_tool does NOT hang and is not skipped in-tree (the D2 skip was a lane decision, never committed); it runs to 26 passed / 1 failed / 27 in ~18 s, the one failure being the stale inline-note assertion recorded further down, which is left intact deliberately.

  • A one-shot product mirrors four boot-trace lines to stderr on every runpit_boot_trace_setup(paths.shop, !daemonized) arms the stderr mirror for every non-daemonized boot, so the shipped mcode binary prints four boot: / [system] lines before it does anything. D1 armed it deliberately (a shipped binary dying elsewhere still says why); it is nevertheless wrong for a gcc-shaped tool, and the fix is a ruling about when a DECLARED one-shot mirrors rather than a change anyone should make in passing.

  • DESIGN GAP, NEEDS JOHN: a product with no logger cannot report its own failureslog.* is a silent no-op with no logger actor (lang/logging.cm:78, “if (!log_target_id) return”), so mcode’s “cannot read foo.cm” reaches nobody and only the exit code carries it. pit product --logger logger/sink ships one, at the cost of the root-shop cart shape. R3a’s “every rung must refuse BY NAME” is not satisfiable for the smallest cart shape until a product has some channel that does not require an actor.

    D3 (R9) surveyed this and did NOT close it. What it needs, exactly: R9’s three refusals land on three different channels, and only one of them is in the gap.

    1. The build-time contradiction (pit product --without <f> against a closure that claims it) runs in the DEV program, which has a logger and a terminal. Not affected.
    2. The realization refusal (a claim of a stripped endowment) is produced as an {error} VALUE on a closure diagnostic and travels back as the start’s reply. It survives with no logger; whether anyone renders it is the caller’s business, which is the correct shape. Not affected.
    3. The boot-time refusal (a binary built without the daemon feature booting a cart that declares one) cannot be a value — nothing has been started yet — so it is a bare fprintf(stderr, ...) in pit_host_main. That is the below-the-logger path, and it already existed for every other pre-boot refusal in that function.

    So the gap is unchanged and is exactly this: log.* from pit code, in a program with no logger actor, has nowhere to go. The mechanism to reach for is already in the tree and already spelt in logging.cm’s own comment (“C’s boot sink covers that window”): pit_boot_trace writes to stderr from C with no actor involved. Closing the gap is (a) an endowment or intrinsic that lets logging.cm’s if (!log_target_id) return branch call that sink instead of returning, and (b) a ruling on whether it should — the logging model was ruled “logger actor + boot hands the token, NO gate”, and a C fallback is a second writer for the same channel, which is the sort of thing that ruling exists to prevent. It is a ruling first and ~20 lines second, and it is deliberately not taken inside a lane about stripping C.

  • Steady-smoke red in the CI container — exits 2 after ~687s; not reproduced locally.

  • heap-test 30s timeout — plus the 5 pre-existing extended-suite failures that predate this arc.

  • RESOLVED — the wave-2a reseed wedge. It was two independent causes and needed both fixes. (a) The enforcing 1 GiB ceiling clamped growth an actor legitimately needs — a copying collector wants several times its live set as room — so the clerk collected 1 GiB for 0 bytes, thousands of times, and never refused. arc/gc-ceiling-fix (d2a6b1cae) makes the limit bound HEADROOM, refuse on LIVE, and never spin; ACTOR_MEMORY_LIMIT is 8 GiB. (b) The seed press held every fleet member’s decoded payloads at once — seed_fleet_entry hydrated all twelve DEVELOPMENT_FLEET executables up front and the cement kept them until the last was pressed, so the clerk’s live set was proportional to the whole fleet. arc/clerk-decode streams it: a resident row travels dehydrated and boot_cart.press_resident_rows hydrates, presses and releases one member at a time, and pool_executable drops the source units the moment the link has taken them. make seed reaches “two clean boot presses are byte-identical” + “promoted clean binary + boot fixpoint” again. Measured on the fixed tree: the hydration path peaks at 650 MB — ONE executable, the largest — and the fleet-prepare phase no longer grows the clerk at all. The clerk’s remaining peak (1450 MB on the cold press, 988/986 MB on the two warm ones) has a different owner: mcode_link.link_result’s streamline and literal pooling for one program, under pool_executable. That term is program-proportional, is the P6 program-granularity ruling’s own cost, and is what any further reduction — or any decision to bring the 8 GiB ceiling back down — has to address. Sizing note for the gc lane: peak live is ~1.45 GiB and the collector gave that actor a 4.0 GiB block (2.8x), so 8 GiB is only ~2x the largest block actually taken. 6 GiB is the lowest limit that still leaves the observed block room today; 4 GiB would clamp it, and only becomes safe once the linker term comes down.

  • make lint is red on dev — 8 platform-include violations, all in internal/os_posix.c (<dirent.h>, <fcntl.h>, <poll.h>, <sys/stat.h>, <sys/types.h>, <unistd.h>, <direct.h>, <windows.h>). The file is portable-core by location but names OS headers directly, which is exactly what the lint forbids; last touched by bf713e39e (“Give linked upstreams the stat short-circuit on realize hits”), so the violations predate the programs arc and no wave-2a lane introduced or worsened them. Verified on a clean dev checkout at 8791b2c91 before any lane merged. The fix is either moving the code into platform/<os>/ or an allowlist entry with a reason — a decision, not a mechanical edit, which is why it is carried rather than done in passing.

  • tests/cli/debug_tool.ce: the inline-note assertion is stale, not broken — the case “the inlining names the function the code was written in” fails with “the inline note does not name spin”. Verified PRE-EXISTING: on a clean dev checkout at 8791b2c91, before arc/debugger merged, the suite reports 18 passed / 1 failed / 19 total with that exact assertion as the sole failure. (On the merged tree the lane’s own additions take it to 26/1/27 — the same one failure, more passes.) The premise is what rotted: the test expects spin folded into middle, but streamline now lowers that edge as a tail call (middletail_call:0), so spin and middle are separate frames and no inline note can name spin. The debugger lane touches no compiler code and the toolchain stamp is unchanged, so this belongs to streamline/B3, not here. The assertion is left intact deliberately — it still describes the behaviour the debugger is supposed to have, and rewriting it to match today’s lowering would erase the signal rather than fix it.

  • CLOSED (arc/gate-hygiene, 2026-08-06) — make lint red on dev, 8 platform-include violations in internal/os_posix.c. Resolved by moving the code behind a platform seam, NOT by an allowlist entry, because the file already had a contract that forbade the drift: source/sys_os.h’s own header comment says the facade exists “so internal/os.c can stay a thin Pit binding shell with no #ifdefs of its own”. The file had simply grown past it — the whole bootstrap_* family (the boot-lane sync-file escape hatch that runs at module top-level, before the dispatcher starts a turn, and so cannot use async io) called open/read/write/stat/opendir/mkdir/rename directly, with #ifdef _WIN32 arms for mingw’s one-argument _mkdir, drive -letter roots, backslash separators, MoveFileExA, and O_BINARY. An allowlist entry would have written “this file may name OS types” over a file whose sibling facades already say it may not, and would have kept the per-OS knowledge in the portable lane where “add a platform” means hunting for #ifdefs. New seam source/sys_boot.h + platform/posix-runtime/source/ sys_boot_posix.c + platform/windows/source/sys_boot_win32.c; the manifests pick the new files up by directory listing, so make manifest was the only wiring. internal/os_posix.c now names no OS header at all and carries a guard comment saying so. Lint reads OK (8 allowlisted core files, no new leakage) — the allowlist did not grow. Diagnostic quality was deliberately kept, not traded: every seam call that had a strerror in its message still has one, threaded through caller-owned err/errlen buffers rather than a shared last-error slot, because catalog writes are concurrent across build workers and a static would race between them.

  • CLOSED (arc/gate-hygiene, 2026-08-06) — catalog_put_value swallowed B4’s named heap-limit refusal. The disruption arm set ok = null and said nothing, so ACTOR_MEMORY_LIMIT refusing by name surfaced one layer up as a bare mach cache write failed (shop_build.cm:967). The reason itself CANNOT be recovered in the arm and never will be: disrupt is a bare keyword carrying no value, and the VM deliberately clears disrupt_msg the instant a handler takes it (source/mach_vm.c — “a caught disruption leaves nothing behind”), because leaving it set made the next bare disrupt report the earlier, caught failure instead of itself. So the fix supplies the JOIN rather than the reason: a note_write_disruption helper logs which write and which key disrupted, called AFTER the unwind (never inside the arm — the disruption being reported may itself be a heap-limit refusal, and allocating a message while the frames that hit the ceiling are live is how one named refusal becomes a second unnamed one). Applied to all four identical store-write arms in shop_store.cm (catalog, object, blob file, tmp), which are one seam rather than a broad sweep.

  • The same swallow shape survives OUTSIDE shop_store.cm — named, not fixed. shop_store.cm:410 open_store’s create() arm sets store_opened = false and discards; a shop that cannot make its own directories reports only “false” to its caller. Beyond this file, the arms worth auditing next are in shop_build.cm and shop_fetch.cm, where the member_memo_key null-concat already hid inside a file() callback for weeks. The general lesson is now twice-proven: in a language whose disruptions carry no value, a silent disruption arm is an unbounded information loss, and the only defence is that every arm SAYS which operation it just ate.

  • mach_pool.c access handler still refuses record operands — the C-side follow-up to B1’s spelling resolution; mach_press.resolved_code resolves spelling rows before C lowering today, so the C side never sees one, but the handler should say what it means.

  • Per-rep re-folding at link/lowering — B1’s generic fold is bounded to tag32’s [-16383, 16383] so it is safe for every rep, which makes it conservative on wider reps. A per-rep re-fold once the target is known would recover what the bound gives up.

  • Retiring the link-plan profile NAME in mcode_link.cm — F1e collapsed the pass sets, but the link plan still carries profile: "ship" as a name. arc/module-exports has landed, so the file is free; the retirement was attempted in that batch and deliberately deferred, because it is bigger than a name swap. profile is a REQUIRED field of pit.mcode.link-plan@1 (mcode_link.cm:82 rejects a plan without it) with 20 producer sites across 11 files, and it does a second job beyond pass selection: it gates granularity validation (dev ⟺ module, ship/boot ⟺ program, mcode_link.cm:104-108), with tests pinning that coupling at tests/mcode_link.cm:269,273. Ruling 5 collapses pass-sets but says nothing about the granularity coupling, so retiring the name needs a ruling on what replaces it. The pass-selection half is already inert: devship (passflags.cm:208) and bootship (mcode_link.cm:2180) make the selection constant today.

Premises refuted by the work these plans ordered

  • work-proposal.md D2 / programs.md phase 2: “press moves out of the shop actor (into the builder worker — retires the +292 KB)” — THE PREMISE IS REFUTED, AND THE ROW SHOULD NOT BE ATTEMPTED AS WRITTEN. Established while landing D2 (arc/product) and re-verified on the merged tip. Both halves of the premise are wrong:
    • The boot press does not run in shop_actor. It runs in pit-shop/clerk: clerk.ce:833 handles the bootstrap message by calling shop_build.bootstrap_artifacts (shop_build.cm:3104), which is what reaches boot_cart.cement_root_cart (shop_build.cm:3193) and through it boot_cart.pool_executable (boot_cart.cm:296,309). The +292 KB was therefore never attributable to shop_actor’s closure.
    • pit-shop/builder_worker is itself a cart executable. It is a member of DEVELOPMENT_FLEET (shop_build.cm:2811-2823, entry 7), so it is pressed into the same development cart the clerk is. Moving the press from one cart executable to another cart executable saves 0 bytes — the press closure ends up in the cart either way. So the relocation D2 was told to perform cannot pay, and D2 correctly did not perform it. Recovering the 405.7 KiB requires a presser that is not in the development fleet at all — a separate program (a product in its own right, or an executable realized on demand and never cemented), not a reshuffle between two members of DEVELOPMENT_FLEET. That is a design decision with a bootstrap question attached (what presses the presser), which is why it is recorded here rather than attempted.

Suspended coverage — owned by a named lane

  • tests/cli/app_bundle.ce and tests/cli/aot_app_bundle.ce are SUSPENDED by D1 (R1); D2 (pit product) owns un-suspending them. Both asserted the app-mode ENVIRONMENT PROBE: pit bundle app copies dist-vm/pit into Contents/MacOS, and the launcher inferred “this is an app — boot it in-process with its own shop” from the .app path. R1 deletes that inference, so a copied binary now carries the DEVELOPMENT cart’s declaration (resident/shop/daemon) and the .app behaves as ./pit does. Nothing in those files can fix it: pit bundle app has to FORGE its binary around a cart that declares one-shot, the way pit cement --app + pit forge --boot already do for a single executable — and no cement mode presses the dev FLEET as a declaring one-shot yet. Un-suspending = delete R1_SUSPENDED in each file and re-point it at the forged binary. Everything else the two prove (bundle layout, one self-contained executable, no development actors, zero compiles from the content QOP, the nested native dylib) is still right and still wanted. The reason goes out on two channels (log.error, verified to reach observe.jsonl; and the errors list, which does not arrive — see the defect below), so this ledger entry is the load-bearing notice.

  • DEFECT, found while trying to make that suspension audible: the tester drops a test_result sent immediately before $stop(). A tests/cli/*.ce actor that sends its own {type: "test_result", ...} and then stops has the message lost — measured directly: a probe sending passed: 1, total: 1 still summarised as passed: 0, failed: 0, total: 0, indistinguishable from sending nothing. The counts such tests do report come from the harness’s fallback branch (shop_tools/test.ce:882-890 — a cleanly stopped actor counts as one pass), not from their own message. Why this matters beyond one file: every skipped test under tests/cli/ is currently invisible. skipped: true renders exactly like a file with no tests, and the errors list that would have printed the reason (summary_view renders errors whatever failed is, so a skip notice could be both visible AND green) never arrives. Not fixed here: shop_tools/test.ce is in the boot closure, so touching it costs a reseed, and this is not D1’s seam. The fix is to reply before stopping, or to have the harness render skipped explicitly.

Working practice learned this session

  • A red perf gate is not a veto on a compiler pass (RULED 2026-08-06). John, retiring tools/compiler_budget.ce: “I question the usefulness of [make budget] and I think we need to remove that concept altogether. We don’t have a budget. Especially if we’re adding new features to the compiler … The point of the budget was when we’re monkeying with a feature we already have, to catch regressions.” And: “these current benchmarks should not be grounds to not go forward with compiler passes necessarily.” Operationally, for perf/ir_census.py and perf/bench_gate.py: a counter that rises because a new optimization landed is expected. Re-capture the baseline and name the pass in the commit message; do not shrink or shelve the pass to make an old number come back. A counter that rises and you cannot say why is the unintended regression these gates exist to find — investigate that one. The rule is written into both gates’ module docstrings and the Makefile’s bench block, because that is where a future session collides with it; this entry is the index, not the copy. The standing other half: the disable levers get built whether or not anything pulls them. pit-compiler/passflags.cm carries one switch per optimization and tests/passflags.cm proves each one changes the output. Selecting a reduced dev pass-set is explicitly deferred — the levers exist so the choice is available later, not so it is made now.
  • The seeded-compiler observation gotcha — a probe run through a live daemon measures the OLD compiler until a reseed makes the new one live. It burned two lanes, and it is why arc/module-exports’s own 475/475 sweep was vacuous. Stage-by-stage evidence in plans/proposal-notes/r5-stone-drop.md. Practical rule: after editing pit-compiler/, ./pit down re-realizes it for scripts, but only make seed makes it the compiler the fleet itself is built from — and any gate whose result depends on emission must run AFTER that seed. Two real defects (below) hid behind exactly this.
  • The static export cache’s staleness is now SOUND under R5. The hoist in pit-compiler/mcode.cm:190-221,3946-3967 never invalidates, which was only safe if a module’s exports could not change after the hoist. R5 freezes what a module main returns, so they cannot — the cache is correct by construction rather than by luck. The full sweep passing with zero write-into-export disruptions is what proves no in-tree module was relying on the mutation the freeze now forbids.
  • export_taxonomy reports declaration/census mismatches. On the 99-unit corpus the declaration path and the retired census disagree on a handful of literal-valued members (archive/zip.cm METHOD_STORE/METHOD_DEFLATE, a 65535 in pit-compiler/compiler.cm) — the census recovered a value the declaration does not carry. ok: true and link_diagnostics: 0, so this costs a little exactness on literal members, not correctness. Worth a look when someone next touches the recognizer.

Pre-existing reds confirmed during B3 (2026-08-05)

Verified by running each on dev @ 8791b2c91’s pit-compiler/ in the same worktree and getting the identical failure — none is caused by the builder diet:

  • tests/network_info.ce and tests/recipes.ce — “compile failed”. network_info FIXED (arc/gate-hygiene, 2026-08-06); recipes fixed separately on arc/feature-strip and deliberately untouched here. Both had the SAME cause, and the compiler was right in both: length is a sensory intrinsic (pit-compiler/mcode.cm sensory_ops) and a sensory intrinsic inside a log.* argument is refused by name — a stripped log must not be the only thing keeping a reading of the world alive. tests/mcode_link.cm:441 asserts that refusal deliberately. The fix is to take the count outside the log call. The same latent break is still spelled in tests/step_debug.ce and tests/shop_requestors.ce (five sites); those files do not currently reach the line, so they are green by luck, not by construction.
  • tests/shop_resolve.ce — bare ps resolves as form: mounted, not through the recipe program table. NOT A DEFECT: it was the seeded-compiler observation gotcha, and a reseed cleared it (arc/gate-hygiene, 2026-08-06). Nothing in this lane touched the resolver. The tell is the test COUNT: red it reported 4 passed / 1 failed / 5 total, and after make smoke reseeded it reports 21 passed / 0 failed / 21 total. It was not failing an assertion so much as running against firmware whose recipe program table predated the current one, so most of the file never ran at all. Practical rule this adds to the one already in “Working practice”: a suspicious drop in a suite’s TOTAL is the cheapest available signal that you are measuring a stale seed — check the total before diagnosing the failure.
  • Reporting gap noticed while chasing the above, worth a fix in whatever lane next opens the harness: while shop_resolve was red, the aggregate ./pit test printed failed: 2 but listed only ONE error line. A failure can therefore be counted and unnamed, which is how a red test hides in a run that is otherwise being read for its error list.
  • tests/mach_pool_emit.cm::test_compact_origin_columns_join_a_pc_in_a_merged_pool — wrong function id. FIXED (arc/gate-hygiene, 2026-08-06): the EXPECTATION was stale, the emitted pool is correct. Panic-tail outlining (77b5a75af, 2026-08-04) gives each unit a second function, so the merged pool carries four, not two; a nested function’s id is its dense index ("0"), not "main". efabb7b11 fixed exactly this two→four drift on the C side (source/mach_pool_test.c) and missed the Pit-side test. Re-keyed on the UNIT rather than the pool index — the assertion now asks the unit what its function is called, which is the claim that was meant and which survives the next change to outlining. Two follow-on index bugs went with it: unit B’s main is now pool index 2, so an index-keyed found_b could never be set, and the two-slot sites accumulator was being written at indices 2 and 3.
  • tests/heap — the 30s timeout. FIXED (arc/gate-hygiene, 2026-08-06): a REAL timeout doing real work, not a lost wakeup. shop_tools/heap.ce sort_by_size_desc was an O(n²) selection sort run over EVERY object in the snapshot, while both callers slice to their limit immediately after — so the whole tail was work nobody could observe. The test dumps the pit-shop/policy actor, whose heap grows monotonically as the daemon serves, so the cost grew with daemon age: measured 9.2 s at ~12k objects and 20.3 s at ~14k, crossing the 30 s deadline under a loaded extended run. ~11.5 s of a ~12 s test was the two sorts. Bounding the sort to the rows anyone reads makes it O(n·want); it also takes a human-facing pit heap <actor> summary from 5.6 s to negligible. heap no longer appears in the ten slowest tests.
  • scripts/check_compiler_tooling.sh — “fresh source press no longer matches the fused generation”. The assertion pins runtime/tests/fixtures/test_good.cm at 3680 bytes / 183 words; both dev and this branch press it to 3664 bytes / 180 words, byte-for-byte the same as each other. The EXPECTATION is stale, which means make check’s compiler-tooling gate is red on dev.
  • perf/ir_census.py gate — hangs at “collecting units 3-4/49” and never returns, on a FRESH daemon, on dev’s pit-compiler/ as well as on the diet branch (4+ minutes each, 0% CPU). It drives repeated compile commands through one pit shell --machine session, which is the same shape as the “pit compile profile wedges on second invocation” defect above. make check’s ir-census gate is therefore red on dev too. FIXED 2026-08-06 (arc/warm-start) — it WAS that defect, not merely the same shape: BATCH_SIZE = 2 over 49 units means ~25 compile starts in one shell session, and the second one wedged in the recursive hydrator. The gate now runs to completion in ~70 s and REPORTS instead of wedging. What it reports is characterised below; the baseline is deliberately NOT rebaselined (a pool-lane press flip is still in flight elsewhere, so a rebaseline now would need redoing).

The ir-census drift, characterised (2026-08-06)

Full gate output: plans/ir-census-2026-08-06.txt. Reference is perf/baselines/2026-08-04-tooling2-ir-census.json @ b8652045a; HEAD is 156 commits past it. Totals: 562691 -> 574474 instructions (+2.1%), 80387 -> 82331 guards (+2.4%), guard_percent 14.2862 -> 14.3315 (+0.045 pp), panic sites 2739 -> 2791, and log_binding_triple_count 358 -> 49 — a large improvement, the only metric that moved far, and in the good direction.

176 failures across 48 of 49 units is mostly gate mechanics, not drift. The gate is a zero-tolerance ratchet: five metrics plus eight guard classes per unit, and ANY increase is a FAIL.

  • 34 of the 48 failing units fail on guard_percent ALONE, by 0.0062 to 0.1250 pp (median 0.0203 pp) — and all 34 got smaller in instruction count. Guards fell more slowly than instructions, so the ratio ticked up.
  • 35 of 49 units DECREASED in instruction count; 14 increased.
  • One failure is structural and independent of quality: pit-compiler/verify_ir.cm was deleted, so the corpus is 49 against the reference’s 50 and the corpus-identity check fails regardless.

Every unit that gained instructions also gained SOURCE (line counts at b8652045a vs HEAD), and every unit whose source did not move went down:

unitinstructionssource lines
pit-compiler/streamline.cm93505 -> 101892 (+9.0%)4994 -> 5482 (+9.8%)
pit-compiler/mcode.cm33450 -> 35907 (+7.3%)4619 -> 4869 (+5.4%)
pit-compiler/inline.cm16941 -> 18602 (+9.8%)900 -> 989 (+9.9%)
pit-compiler/compiler.cm22204 -> 23831 (+7.3%)1717 -> 1891 (+10.1%)
shoplib/mach_press.cm1009 -> 2385 (+136%)143 -> 251 (+76%)
pit-compiler/numeric_literals.cm254 -> 1312 (+417%)42 -> 147 (+250%)
pit-compiler/panic_outline.cm5709 -> 6278 (+10.0%)295 -> 345 (+17%)
shoplib/debug_resolve.cm11291 -> 11766 (+4.2%)700 -> 751 (+7.3%)
pit-compiler/fold.cm16135 -> 16584 (+2.8%)1094 -> 1180 (+7.9%)
pit-compiler/callresolve.cm15799 -> 16183 (+2.4%)767 -> 799 (+4.2%)
pit-compiler/tokenize.cm8803 -> 9087 (+3.2%)528 -> 562 (+6.4%)
shoplib/actor_find.cm955 -> 976 (+2.2%)128 -> 137 (+7.0%)
pit-compiler/structured.cm27359 -> 27381 (+0.1%)1113 -> 1115 (+0.2%)
pit-compiler/parse.cm27186 -> 27195 (+0.03%)2374 -> 2378 (+0.2%)

Controls: record_shapes.cm, forward.cm and pitfs.cm have IDENTICAL source at both commits and all three went DOWN (-16, -15, -17). Largest decrease is pit-compiler/stream_ir.cm, 5059 -> 4656 on +7 source lines.

Verdict on the two hypotheses.

  • “B1 added resolved_code to mach_press.cm, so its 1009 -> 2385 is source growth, not fold loss” — CONFIRMED: the unit’s source grew 76%. It is the densest of the risers (7.1 -> 9.5 instructions per source line), but the direction and the order of magnitude are source. Same story for numeric_literals.cm, whose source TRIPLED: its +417% is B1’s own implementation, not B1’s effect on anything else.
  • “B1’s restricted constant folding makes numeric code emit more instructions, predicting broad modest increases concentrated in numeric code” — NOT VISIBLE, and not needed to explain anything. The prediction is broad increases; the data is 35 of 49 units DOWN, and all three units with byte-identical source went DOWN. If unfolding were broad, untouched numeric units would have risen with no source change; none did. Either the effect is below the noise floor of ordinary source churn over 156 commits, or other optimizer gains in the same window offset it.

Nothing moved in a way source growth does not explain, so no unit is named for investigation. When the press flip lands, the rebaseline is routine — but a rebaseline alone will not stop the guard_percent hair-trigger firing again on the next round of churn. That is the gate’s own design (a ratio with no tolerance band), and it is a separate question from this drift.

The final integration (2026-08-06, arc/final)

The seven-branch merge (gate-hygiene · preempt-loops · byte-budget · clerk-decode · hot-reload · feature-strip · function-ref, which carries pool-lane and one-pool), reseeded to fixpoint and gated. What it added to this ledger:

  • CLOSED — the guard_percent hair-trigger. Named in the drift note that ends the section above, fixed in the same pass as the rebaseline: the ceiling now carries a 0.25-point tolerance band (QUALITY_TOLERANCE in perf/ir_census.py), the only ratio in the table and the only entry with a band. Both of its terms stay exactly gated, and the one real density change in the batch (shoplib/mach_press.cm, +4.35 points) is still red.
  • perf/trend/baseline.json no longer carries stages.* ceilings — 83 rows, not 114. pit compile profile wedges on second invocation (OPEN, above), so the lane cannot be measured at all; the alternative was committing frozen ceilings for a lane nobody can run. A full perf/run_bench.py now fails the gate with 31 unrecorded metric lines until that hang is fixed and the baseline is re-captured without --skip-stages. That message is the ask.
  • tests/freeze_module_loop.ce is flaky under full-suite load — new with arc/preempt-loops, green alone and green in two of three aggregate runs, disrupted in the third. The assertion is a bounded ATTEMPT COUNT (FREEZE_ATTEMPTS = 40) on catching a spinning worker, and 2400 other tests competing for the same cores is exactly the condition that spends attempts without the worker being scheduled. Not a runtime defect and not a reason to hold the landing; the fix is for the bound to be load-aware (or for the test to retry until it observes the worker running at all) rather than a constant.
  • The stale-observation gotcha recurred at the merged tip, and the TOTAL rule caught it exactly as written. After make seed, tests/shop_resolve.ce reported 4 passed / 1 failed / 5 total and the aggregate ./pit test was 2398/2 then 2399/1 — with, again, one failure counted and unnamed. A make smoke cleared it: shop_resolve went to 21/0/21 and ./pit test to 2416 passed / 0 failed / 2416 total. Note the aggregate total moved 2400 -> 2416, which is the same tell one level up. make seed is not sufficient on its own — it promotes the binary and the boot artifacts but leaves the local shop’s recipe/program table as it was, so the first observation after a reseed can still be taken against the old one. Run make smoke after make seed before believing any suite count.
  • shop_resolve SETTLED at the arc/integrate-budget merge (2026-08-06), in favour of the reseed explanation. Still NOT A DEFECT. Two lanes had reported opposite things at nearly the same base — one that make smoke cleared it, one that make smoke did NOT and the failure survived with its own changes stashed. At the merged tip (arc/retire-budget into dev@162d10de5), after a clean make smoke on a daemon brought down and confirmed gone by socket path, tests/shop_resolve.ce is 21/0/21 on three consecutive runs, and the aggregate is 2426/0/2426. make nuke was NOT needed and was never run — so “only nuke clears it” is not the shape of this one. Nothing in this lane touched the resolver. The mechanism, which the earlier entries above did not state and which makes the TOTAL rule sharper: this harness is SEQUENTIAL and fail() calls finish(), so the first failing step ENDS THE RUN. total is therefore how far it got, not how many tests exist — there are always 21 step(...) calls in the file. 4 passed / 1 failed / 5 total means step 5 failed, and step 5 is "shop recipe program roots provide bare commands" (line 191), which is exactly the bare ps resolves as form: mounted symptom both lanes reported. So a suppressed total is not evidence of staleness specifically — it is evidence of an EARLY failure, and staleness is one cause of it. Read the total as “it stopped at step N”, then look at step N. The corollary for the aggregate’s failed: 2 printed with one error line is the same shape: a sequential file that aborts reports one error for a run that counted more.
  • The boot press is still on the static lane, and function-ref left the reason in the code. pit-shop/shop_build.cm’s seed-fleet path now carries a comment block naming the two obstacles: (1) that function feeds two consumers of one closure — the QOP universe a dev daemon adopts and lowers per module (which must stay static until the dev lane moves onto pools, because one pool per unit cannot resolve a finalized LOAD_MODULE_RESULT) and the resident_executable the root cart presses (which wants the pool lane), so the boot lane needs a SECOND realization under the pool-lane key; and (2) wiring exactly that was tried and measured — the root exec presses correctly (28 units, 24 finalized sites, one pool section) and the bootstrap then answers true with exit 1, writes no artifact and presses no resident, with no named refusal anywhere, reproduced on three seeds. That missing diagnostic in cement_root_cart’s tail is the piece of work, and it sits directly in front of the ruled next step (flip the dev realize lane to the pool lane, then delete the static lane).

The pool-lane flip attempt (2026-08-06, arc/pool-lane-flip)

R7 (move the dev realize lane onto the pool link lane) + F4 (delete the static lane) were attempted together. Two defects in front of them were found, fixed and landed; R7 itself did not land, and the reason is now a measured fact rather than the guess this file previously carried.

Landed

  • The “bootstrap answers true with exit 1 and no named refusal” mystery was a FLATTENING, not a missing diagnostic. pit-shop/clerk.ce’s bootstrap handler wrote reply_failure’s RECORD ({ok, error, problem, reason}) straight into error, and text(record) is “true”. The refusal was always there; one hop before the client it became a boolean. Fixed to flatten with shop_util.problem_text / problem_diagnostic_messages, and the whole tail from boot_cart.cement_root_cart to the bootstrap client now names its stage and its member: each of the four presses is guarded, press_resident_rows says “root cart resident 6/12 ‘pit-shop/courier’”, bootstrap_artifacts keeps a belt around the synchronous cement, and shop_tools/bootstrap.ce refuses a non-record reply by name instead of forwarding it.
  • link.missing_member was a wrong refusal and broke pit cement --app TODAY, at dev tip, with no other change in the tree. mcode_link failed the link when a consumer loaded a member a COMPLETE stone export did not carry. Reading an absent member is ordinary ƿit and yields null, and net/net.cm uses that as a feature test (is_function(net_impl.provider) ? net_impl.provider() : ..., where net/net_darwin.cm exports {config} and no provider). So every press of any program reaching net.cm refused, twice. ./pit cement --app pit-shop/courier reproduced it in one command. The site now keeps its guarded load and counts as generic; tests/mcode_link.cm pins the new behaviour with two tests in place of the one that pinned the old.

R7 did NOT land, and the previously-carried reason was wrong

  • “One pool per unit cannot resolve a finalized LOAD_MODULE_RESULT” is not the blocker. Measured: ./pit cement --app pit-shop/courier --pool-units unit presses that program into 30 separate pools and self-verifies. The emitter’s covering carries {unit: <plan slot>} per import and MACH_LOAD_MODULE_RESULT reads results[base + bx] with base 0 against the actor’s own result array, so the number resolves across pool boundaries. Per-unit granularity works.
  • The real blocker is the COVERING’S IDENTITY. A per-unit pool’s covering is {unit: <plan slot of the provider>}, which is a property of the ENCLOSING EXECUTABLE’s start plan. The dev lane’s Mach payload is content addressed by K_mach = (mach ABI, lowering identity, mcode content hash, label, numrep, endian) — shop_store.mach_derivation_key — and is therefore SHARED by every executable that uses the module. Giving the dev lane a covering makes each module’s Mach payload executable-specific and destroys the cross-program Mach cache, which is most of what makes the dev loop fast.
  • The executable-independent alternative is refused in C. The bound form (import_op_bound: an IMPORTS row keyed by stable_binding_key) does not depend on the enclosing plan — and source/image_provider.c:1138 (resident_function) refuses it outright: “a start-plan window may not declare cross-image imports”. Every unit, dev lane or cart, runs through that door (source/start_plan.c plan_run_pool). R7’s actual granularity work is therefore: let the boot walker bind a unit’s cross-image import rows from the providers it has already produced (it holds every one of them in values[]) before resident_function checks them. That is C work in the boot floor and it is what F4 waits on.
  • What the flip looks like when it is tried: link_mode = "pool" unconditionally in shop_build.prepare_simple_bundle (plus the two other link reads), context.link/build_msg.link constant in shop_realize, mcode_cache_key folding the constant “pool” (keep it — dropping it collides with a pre-R7 shop’s static entries). The seed then fails at press 2 with mach_pool_emit: unit 0 import 0 (lang::messaging) was not covered — the press placed no provider for it, which is the emitter being right. The patch is short and worth re-deriving rather than saving.

Carried out of plans/todo.md at its retirement (2026-08-07)

todo.md moved to plans/archive/todo.md under work-proposal.md §5. Every item in it was triaged first; nothing was dropped silently. What survives is below, what died is named at the end with the reason.

Still real, carried

  • The image-arm closure-upvalue cost, 4.6×. closure_calls runs an identical instruction stream at 8.76 ns/i on the pool lane vs ~1.9 on the deleted register lane; isolated to bind_getup. Start: make bench shows the row — kernels.closure_calls.* in perf/trend/latest.json, from benchmarks/bench_suite.ce. The lane is mach_pool.c’s pool-adopted functions — compare how a pool function reaches its upvalues vs how the register tree did. Read the right number: the win is ns/instruction, and that is a WALL row, reported and never gated (closure_calls has been seen at 16.4 ms and 4.0 ms on the same 1.8 M-instruction stream). The gated kernels.closure_calls.instructions ceiling is what proves the instruction stream did not change while you were fixing how it executes — exactly the guarantee this item needs.
  • Cart shrinking / bytecode dedup. Start: pit cart info on boot/root.cart for the section inventory; dedup candidates are identical pools across executables (the content-hash directory rows make them visible).
  • zz_shop_tool_smoke’s deadline. Remeasure cold on a QUIET machine: the in-flight numbers were load-polluted (181–188 s against 180 s). Start: invalidate the toolchain stamp (touch + revert a pit-compiler/ file), ./pit down, run it solo, time it; then rule a class or slim the test (it starts nine tool actors).
  • The parked concurrency experiment. PIT_TEST_CONCURRENCY=N is committed and daemon-read; the sweep never produced clean data (machine contention, then the observer deadlock it helped find). Resume on a quiet machine: widths 1/4/8 ×2, fresh daemon each, an orphan check between runs, and treat any suspiciously short wall time as a wedge, not a result.
  • The client-teardown/orphan class. Completed pit clients sometimes park in teardown (a pthread_join in exit_handler), surviving their parent’s kill. Not yet deterministic. Start: tests/cli/debug_tool.ce runs, then lldb any 0%-CPU survivor at exit_handler and find what thread it joins. Two sightings are already recorded above — the pit debug sites worker leak and the stranded pit forge on the worktree socket — and a third landed 2026-08-07 during this very cleanup (a make that exited 0 still left a pit forge darwin --out build --boot boot/root.cart holding .pit/pit.sock, so the next ./pit refused on build signature). The recovery in the foreign-socket entry above worked verbatim.
  • pit.bootstrap.target@1’s ~140 spec lines (docs/spec/artifact-formats.md:222+) describe a record with no producer and no reader anywhere. NEEDS A RULING: dead design (delete) or pending target-truth (keep)? Docs are target-final, so absence of code alone does not decide it.
  • A second codename pass over the adjacent series the first sweep’s pattern missed: CAKE1, FRESH1/2, SHOP1, AUDIT1, CART3/4, WOTA1, NANO1, BUILD1, singular “edict N” — same translate-don’t-delete method, same glossary style. (L2b is off this list: killed 2026-08-07, see below.) ~an hour of agent work.
  • T020’s gate was skipped. The meson parity oracle was deleted without the console-lane comparison ever running, so cake is unverified against any oracle for the console lanes. Recorded, not urgent — the console lanes get real verification when the platform rollout reaches them.
  • docs/shop/c-modules.md names recipes that do not exist (macos_arm64/macos_x86_64/linux_arm64, 5 sites, verified still present 2026-08-07); the real names are darwin/linux/etc. Mechanical fix.
  • The word “lane” does double duty — the rulings’ “one execution lane” (docs/spec/mcode.md:14) vs the compilation lanes at mcode.md:133-142 (pool vs static-linked; both real). Worth one glossary sentence. Note that R7’s static-lane deletion may resolve it by removing one of the two meanings.
  • PitMachLoadStatus (source/pit_internal.h:222-227) is a typedef with no uses anywhere — the last surviving entry of the L4 dead-code inventory.
  • Platform rollout, systematically (expands the one-liner above). The model L6 proved: a platform = a recipe file + a platform/<os>/ folder + a pressed boot pair + a CI runner. Windows first needs the runner ruling. Consoles (playdate/n64/ps1/dc/psp) have recipes and cross-press works for them; each needs its platform folder completed and a way to run the suite. Start from docs/architecture/ target pages and the L6 commit messages.
  • Arc close-out chores not yet demonstrated: total the deletion bill across L3/L4, and demonstrate the compiler-less runtime (a pressed cart boots and runs on the vm_only shape). The guard-comment sweep rule lives in plans/method.md.

Do-not-delete note (kept, not an action)

  • runtime_code_count / code_cache_* counters survive their cache ON PURPOSE (source/scheduler.c:3349; tests/runtime_arena_counters.cm pins them at zero as L3’s observable). Do not sweep them as dead.

Deferred rulings — each needs John, none urgent

  • A reap should carry its own reason. Today a reaped actor and a voluntary $stop() reach the overling identically; tests/reap_is_not_a_pass.ce pins the current sameness. Lifetime-model honesty says distinguish them.
  • A daemon whose listener halts should exit. Services now state their lifetimes so the reap-zombie is fixed, but a listener death still leaves a live unreachable process by other paths. The honest end is exit.
  • - true error rendering. A failing test with a long reason renders as bare - true in the human errors list (the real reason is only under --machine). One small fix in the test harness’s error formatting.

Died at the triage, with the reason

  • “L6 lands (in flight)” — L6 landed; the 3-platform cross-press is confirmed and its CI workflow is committed. Nothing left.
  • “L2b drain tail”KILLED BY RULING 2026-08-07. PGO may return later. See the L2b entry below.
  • pit-shop/mach_lower.cm’s stale PIT_POOL_PAYLOAD=1 state block — already deleted. The file’s surviving comment is a legitimate reseed warning.
  • Stale “pit.cart@1 trailer” comments at source/cart_boot.c and cake/manifest.cm — already fixed; grep finds no pit.cart@1 at either site.
  • The allow_compile contradiction in pit-shop/policy.ce — already resolved; allow_compile does not appear in that file. The diary’s “deleted 2026-07-22” record was right.
  • The storage revisit (boot/content grew to ~86 MB) — dead as written. There is no boot/content directory; the boot tree is root.cart (15 MB), engine_lite.mach, manifest/, targets/. If storage is revisited it starts from a fresh measurement, not this number.
  • “Game engine work” — the entry itself said “not tracked here”. Unblocked since L3; it lives outside the plans.
  • “Compiler-generated mcode quality” — not carried because it is not homeless: plans/compiler-vision.md is the live queued arc for it.

Found live that the inventory called dead

  • Pit_MachPoolInstruction is LIVE. The L4 dead-code inventory listed it for deletion; it is called at source/mach_pool.c:1576 and :2005 and declared at source/mach_internal.h:53. Do not delete it on the strength of that inventory.

L2b (the PGO drain) — KILLED BY RULING 2026-08-07

John: “let’s just kill the l2b pgo drain … right now it can just be removed.” PGO may return later; this closes the carried item, not the idea.

  • The rescued emitter branch l2b-pgo-emitter-stash is deleted. Its tip was 5984b11a12c7565a45c73d726d4064add8f303bf (“On l3-continuation: L2b: PGO_SITES emitter (recovered; must land WITH the C counters)”) — recoverable from the reflog by that hash, or by re-deriving, which the original entry already said was the honest path (“the patch is short and worth re-deriving rather than saving”: D5 deleted the only production path that built a PGO collector, so step 3 was a rebuild, not a port).
  • pit-shop/pgo_store.cm is NOT dead and was NOT deleted. It is the PGO application half, and it has live consumers: pit-shop/shop_build.cm:11 (load_exact / load_file at :2429,2431), plus tests/pgo_store.cm, tests/pgo_executable.ce, tests/cli/pgo_store_atomic.ce and tests/fixtures/pgo_store_writer.ce. L2b was about collection. Likewise pit-compiler/pgo.cm (used by pgo_store.cm, shop_build.cm, pit-shop/clerk.ce) and $pgo.count_instructions (the kernels bench lane, tests/compile.cm, aot_bench/, tests/actor_module_surface.ce) stay.
  • The runtime PGO_SITES plumbing also stays: source/mach_pool.c, source/pit_internal.h:266, shoplib/mach_pool_emit.cm:611-617 and source/mach_pool_test.c all read or rebase the section, and the C test pins its accessor. Nothing writes a PGO_SITES section today — that absence was the drain — but the reading side is exercised and is not free to delete.
  • What was actually removed: the L2b prose in the plans and the “until L2b’s drain lands” framing on the two refusals. pit-shop/terminal.ce:2418-2432’s --pgo_collect refusal and pit-shop/mach_lower.cm’s note now say the refusal is the standing answer rather than a countdown to a queued landing.

shop_resolve red again on a fresh worktree, and make smoke did NOT clear it (2026-08-07)

New datum for the settled shop_resolve entry above, recorded during arc/constraint-cleanup. Not caused by that branch — see the evidence.

The sighting. In a fresh worktree after a cold make + full reseed:

tests/shop_resolve.ce         4 passed / 1 failed / 5 total
./pit test (aggregate)     2409 passed / 1 failed / 2410 total

- shop_resolve: bare ps should resolve through the recipe program table:
  {"package":"ps","executable_hash":"blake2:32f7472...","kind":"program",
   "locator":"ps","form":"mounted"}

The arithmetic closes exactly, which is worth keeping because it makes the aggregate a usable detector: the healthy file is 21/0/21, the degraded file is 5 total, and the expected aggregate is ~2426 against an observed 2410 — 2426 - 2410 = 16 = 21 - 5. The whole aggregate shortfall was this one file taking its short path. An aggregate TOTAL that is low by exactly one file’s missing tests is the tell, and it points at the file without needing a per-file diff.

What is NEW: make smoke did not clear it. The entry above records make smoke promoting shop_resolve from 5 total to 21/0/21. Here smoke ran to smoke: OK twice, before separate observations, and the file stayed at 4/1/5 both times. So “reseed staleness, cleared by smoke” does not cover this instance. Either there is a second cause with the same signature, or the fresh-worktree cold-reseed path reaches a state a warm reseed does not.

Why it is not the branch’s doing (checked rather than assumed): the whole non-plans diff of arc/constraint-cleanup over pit-shop/, lang/, std_endowments/, pit-linker/, shop_tools/ and tools/ is comments, with exactly one non-comment line — the corpus path in tools/export_taxonomy.ce, a standalone tool that no suite runs. tests/shop_resolve.ce is untouched. Nothing in the branch can reach shop resolution.

Do not treat this as green. The earlier “settled, not a defect” verdict was reached on the reseed explanation; this instance refutes that explanation for at least one path, so the question is reopened to that extent. The next lane that sees shop_resolve at 5 total should capture whether a SECOND full make smoke (or a make nuke) clears it, and on what kind of shop — that is the missing observation.

The missing observation, taken on arc/crypto-locator (2026-08-07). Fresh worktree off dev e82cf985d, local make shop. Signature identical down to the executable hash (blake2:32f747286d90c46748226c57100bd09efd5182828c199e30...): tests/shop_resolve.ce 4/1/5, ./pit test 2409/1/2410 — the same exact-16 shortfall. A second full make smoke does NOT clear it. Both smokes reached smoke: OK; the aggregate was 2409/1/2410 before and after, unmoved. Two further facts that narrow it:

  • It is not the ./pit ps that make smoke ends with mounting the program. After ./pit down (socket confirmed gone), on a daemon that had never run ps, ./pit test run tests/shop_resolve.ce was still 4/1/5.
  • The aggregate TOTAL is stable at 2410 across all observations, so this is not the moving-total stale-observation tell. It is a steady state, not a race.

So “reseed staleness, cleared by smoke” is refuted for this path on a second independent branch, and the daemon-session/mount explanation is refuted too. make nuke remains untried — that is now the missing observation.

The first dev push to gitea in ~64 commits (2026-08-07, runs 425–430)

Read from the server with scripts/ci_runs.sh, at 34fdd380b. “Basically everything failed” is accurate, and most of it is one mechanism, so the count of red jobs badly overstates the count of problems.

The mechanism: a Dockerfile-changing push races its own image

.gitea/ci.Dockerfile changed in this push. That fires build-ci-image.yml — and it fires ci.yml, platform-vm.yml and console-image.yml at the same instant, because gitea has no way to express “that workflow first”. The image finished publishing at 17:05:29Z; every container job had already started and pulled. act_runner does not treat a failed pull as fatal:

could not update image 'gitea.pockle.world/john/pit-ci:latest' (),
continuing with the local copy: Error response from daemon: manifest unknown

One line, buried in the runner preamble, and then eleven jobs ran a pre-v2 toolchain wearing this commit’s face. pit-ci:latest does exist now (registry says created 2026-08-07T17:05:29Z), so a plain re-run of ci.yml reclassifies most of this.

scripts/ci_image_check.sh + the /etc/pit-ci-image-tag stamp now make this say itself in each job’s first lines. It reports, it does not gate — a stale image is not always wrong, and converting a confusing red into a red buys nothing.

pit-console-ci was never published at all

Different failure, same shape. console-image.yml consumes pit-console-ci:latest, built from ci/Dockerfile — and no workflow builds or pushes it. The registry has pit-ci and pit-pages-ci and nothing else. So the label resolves to whatever that host cached by hand, which has no node, so actions/checkout@v4 cannot run:

exec: "node": executable file not found in $PATH

image-sanity passed only because it has no checkout step. Two things are missing and both are needed: a build-console-image.yml alongside the other two image workflows, and nodejs in ci/Dockerfile (every JS action needs it).

Real defects, image-independent

  • shoplib/wota.c had no <limits.h> — FIXED on arc/ci-postpush. wota.h only pulls <limits.h> inside its #else /* PIT_WOTA_BITS == 64 */ arm (line 184, nesting depth 2 under the #if at line 41), so on any 32-bit-wota profile the translation unit that uses INT_MAX at line 428 sees no declaration. gcc refuses; Apple’s libc leaks INT_MAX through <stdlib.h>, which is exactly why this only ever showed on Linux and why the macOS arms got past it into runtime failures instead. Every nan32 and fixed32 build has been broken on Linux since the wota32 split — that is check-profilesnan32, fixed32 and nan32-compact arms, i.e. the playdate and console rungs.
  • scripts/check_playdate_api_coverage.py chased a moved file — FIXED. main_playdate.c went under source/ in 01bba7317 (2026-08-02) and the script was not followed; playdate-sim died on a bare FileNotFoundError traceback. Now source/main_playdate.c, and a missing entry is a named refusal listing every stale path in one pass.
  • scripts/check_compiler_tooling.sh needed xxd — FIXED. ubuntu 24.04 split xxd out of vim-common; the CI image does not carry it. The pipeline produced a 0-byte fixture and reported it as three unrelated pool failures (fixture pool size 3480 vs 0, truncated pool header twice) — the actual cause was one line above, xxd: not found. awk now emits the bytes itself under LC_ALL=C; verified byte-identical to the xxd output (3480 bytes) before the swap. The image still has no xxd; deliberately not added, since nothing needs it now.

Open, needing a re-run on the correct image to classify

None of these can be called until ci.yml runs again on pit-ci:latest:

  • webcheck-web: target seed FAILED after the presses linked. The emsdk version is one of the things the stale image got wrong, so this is the most likely pure artifact of the race.

  • windowstarget stopped, exit 1, after linking the shop tools under wine. Same suspicion.

  • web-vm (platform-vm) — app content publish failed: closure incomplete for runtime::tests/file_provider_surface.ce: 16 missing, all mcode_metadata for lang-endowments/* under one parent digest blake2:0e8b8853…. This one reads like a real closure/linker defect rather than toolchain drift; if it survives the re-run it wants the export-recognizer lane’s attention, since per-member export exactness is what that arc touched.

  • cross-check, all three jobscross-run-linux got as far as forge: tests 18 passed twice and then forge: candidate bootstrap failed / compile failed pit-shop/daemon_transport.cm: target is gone, dying in make smoke’s seed. target is gone during a fleet compile is the shape of a builder actor dying, which on this host could equally be the stale image or contention — thirteen container jobs were dispatched at once against a runner at capacity 3. Re-run before reading anything into it.

Open, NOT explainable by the image (bare-metal macOS)

  • check-arms: gc_policy fails on every 32-bit-value arm. numrep-nan32, numrep-fixed32, nan32+compact, nan32+kim8 all exit 1; fixed32+kim8+compact exits 134 (SIGABRT). Every 64-bit arm is green, including validate-gc and force-gc. The macOS runner is bare metal — no container, no image — so this is real and it is not new; it is newly visible, because 6c68f0a47 promoted macOS to gate dev pushes and make check never runs check-arms at all (CLAUDE.md says so explicitly). Diagnosis pointer: source/gc_policy_test.c gates its block-sizing cases on gc=compact only (the copy_policy flag, ~line 665) and has no gate on value width, while the 32-bit arms also carry PIT_MAX_IMAGES=2 and halved region defaults. The assertions are absolute byte sizes (expected soft cap %zu, got %zu). Most likely a missing profile gate or size expectations pinned to a 64-bit heap; the SIGABRT arm needs looking at on its own terms. The per-arm logs are the thing to get — check-arms keeps them under $TMPDIR/pit_arms.* and CI does not upload them, which is why this entry cannot say more than the exit codes. Uploading that directory as an artifact would pay for itself the first time.
  • benchbench.run: BENCHRUN line is not JSON: Unterminated string starting at: line 1 column 8158. A single ~8KB JSON line arrived truncated. Could be interleaving with concurrent actor output on the same stream rather than a bench defect; perf/run_bench.py should be made to say what it actually received when the parse fails, rather than quoting the parser.

deploy-site never ran, and why that was the wrong design

Its if: was satisfied (push to dev). It was skipped for needs: [linux, bench], both of which failed. Changed on arc/ci-postpush: needs: is kept for ordering but always() makes it ordering only. The site is where the red is READ; a cockpit that goes dark exactly when something is wrong is measuring the wrong thing. scripts/fetch_bench_trend.sh already falls back to the checked-in sample when no trendline was published, so a red bench costs the deploy nothing.

Follow-on the playdate fix exposes. With the traceback gone the coverage gate produces a verdict for the first time since 2026-08-02, and against the local SDK (3.1.1) that verdict is TOTAL: 533/553 covered; 4 excluded; 20 missing — exit 1. CI pins 3.0.6, so the CI number is not knowable from here and several of the 20 read like 3.1-era additions (getSentBytesPending, getOutputAsSource, getPowerStatus). Two separate things to settle, and they should not be confused: whether playdate-sim is green on 3.0.6 (read the next run), and whether the SDK pin should move to 3.1.1 and those 20 be covered.

A cart carries no target identity, and the root mount does not check (2026-08-07)

Found by arc/docker-boot while root-causing “pit does not boot in Docker”. The container half is fixed; this half is not, and it is the reason that took as long as it did.

The claim. Nothing in the system compares which target a cartridge was pressed for against which target is mounting it. Two independent gaps:

  1. source/pit_profile.h:204PIT_PROFILE_STRING is name;value;rep;endian;text;record;gc;wota. No system, no arch. Verified by reading the stamps out of both committed seeds: the Darwin boot/root.cart and the Linux boot/targets/linux/root.cart are byte-identical at CART_OFF_PROFILE_STAMP (host-nan64-v1;value=64;rep=nan64;endian=le;text=utf32;record=hash;gc=copy;wota=64).
  2. source/cart_boot.c:206 — the stamp compare is if (!trusted && ...), and cart_mount_root_image (source/cart_boot.c:311) passes trusted=1. The root cartridge’s stamp is therefore never compared at all, on the stated ground that “the root image was produced with this binary’s own seed”. The cold floor is the case where that premise is false: boot/manifest/bootstrap.sh:570 checks only that boot/root.cart is non-empty before .incbin-ing it into a binary it compiles for $SYS.

What it costs. A Linux binary holding a Darwin cart mounts it, breeds the shop actor, and the boot walk refuses at engine/boot_walk.cm:302cannot start actor: required endowment $darwin_file has no provider 'endowment/darwin_file' in this target. The fleet quiesces with no listener. Every client burns the full readiness timeout. The refusal now reaches boot.log (fixed on this branch), so the failure is audible — but it is heard several layers below where it was decided, and it names one endowment rather than the mismatch.

The fix, and why it is not taken here. Adding ;system= (and probably ;arch=) to PIT_PROFILE_STRING and un-trusting the root mount would make this refuse by name through machinery that already exists — cart_profile_diff names the differing axis, so the message would read cart profile mismatch on system: ... vs .... It is the right shape. It is also a stamp change: every committed seed (boot/root.cart, boot/targets/*, boot.qop) is invalidated and has to be regenerated on both platforms, and the “trusted root” ruling is a ruling. Needs John, and needs a lane that can reseed both targets. Not a drive-by.

Do not re-raise as “pit is broken in containers.” It is not, and never was: the same two artifacts fail identically on bare metal. See benchmarks/shootout/README.md, “The seed the container builds against”.

make smoke does not clear a COLD-FLOOR daemon (same lane, 2026-08-07)

Sharpens the carried “make local lacks daemon-fresh” item above, which says “only make smoke and make seed restart it”. From a checkout with no ./pit that is not true of smoke, and the reason is prerequisite order:

smoke: local-shop-setup manifest-fresh all daemon-fresh

manifest-fresh (Makefile:290) runs ./pit, and on the cold path the daemon holding the socket at that moment is the cold-floor one that just did the forge, while ./pit is the build/pit forge promoted over it. So smoke dies at manifest-fresh with

pit: shop daemon has different build signature
pit:   daemon=cold-20260807170735  current=forge-host-nan64-v1
make: *** [Makefile:290: manifest-fresh] Error 1

before daemon-fresh — the step that would have fixed it — is ever reached. Observed twice in a container, and it is the same shape as the standing “run plain make first in a fresh worktree” advice, which is how it has been survivable this long. The working sequence is make, then ./pit down (tolerate its exit: it reports a timeout when the endpoint outlives the stop, having already stopped the daemon), then ./pit ps. That is what benchmarks/shootout/run_shootout.py now does.

Not fixed here — reordering smoke’s prerequisites, or making manifest-fresh depend on daemon-fresh, touches the dev loop every lane is using right now.

Source: plans/carried.md