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=256daemon-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_falseinplans/archive/qop.cmfn 56 — a real emitted branch with no target; parked because the file is archived evidence.make locallacksdaemon-fresh— plainmakecan leave a daemon serving the previous binary; onlymake smokeandmake seedrestart it.Foreign-daemon-socket trap — with no
.pit/of its own, a worktree’s./pitbinds the socket owner’s shop, somakebuilds into that worktree. Run plainmakefirst in every fresh worktree.A
makecan wedge with no children and no CPU, and killing it strands apit forgeon the worktree socket. Hit during the D2 integration (2026-08-06,arc/integrate4).make -j8in a fresh worktree ran far enough to produce./pitand.pit/, then sat for 12 minutes in stateSwith 0:00.03 CPU, zero children and no submake — nothing was building and nothing was going to. Killing it left apit forge darwin --out build --boot boot/root.cartreparented to init and still holding.pit/pit.sock, so the nextmakerefused withpit: shop daemon has different build signature pit: daemon=cold-20260806005034 current=forge-host-nan64-v1 pit: preserving endpoint because its process is not confirmed deadwhich 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.sockand.pit/pit.pid, then re-runmake— 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 inmakeitself rather than in a./pitclient, 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:1132realizes every actor start with nolinkin the request,shop_realize.cm:1068therefore leavesbuild_msg.linkunset, andshop_build.cm’slink_modefalls 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 finalizedLOAD_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.canonicalis NOT static-lane residue, and §5.2 is wrong about it. The inventory listsstatic_bindingson the executable module row among the deletions. Itsexportssub-map and the cart NAMES/IMPORTS rows are residue;canonicalis now load-bearing for the press: it is the only request→provider map that covers NATIVE providers. The manifest’sbindings[]rows keep an edge only when the provider is itself a manifest UNIT (executable_manifest.cm:329-334), souse('blob')— every extension — has no row there, whilebuild_exec_plangives it an ordinaryENTRY_NATIVEplan slot. Covering the press from the manifest alone refuses on the firstblobimport. Whoever runs F4 must keepcanonicalor 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.cenow passeslink: "pool"; the pressedmcodeproduct 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, becauseinline_budget: "none"leaves the call-directness payoff uncollected; that is CP2’s half, below). The fault was NOTsource/mach_pool.c:4590. Thefunction_reflowering 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_functionread["get", d, s, 1]in any function as naming the module main’s frame. Depth 1 is the main only for a DIRECT CHILD, andpit-compiler/mcode.cmis 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 handedrecordlink facts for slots holding numbers (gen_functiongot two). The linked streamline then foldedis_numto false and the guard’s outlined panic became unconditional — hence<compiler panic: operands must be numbers>atgen_function. Fixed bymain_lexical_depths(agetresolves 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 onlink_types, which is keyed by slot and applied by streamline at everyimport/member_import/getwriting that slot. Pinned bytests/mcode_link.cm::test_import_origin_follows_lexical_depth_to_the_mainand bytests/pool_link_run.ce, which presses a multi-unit program with a finalized member into one pool and RUNS it — the gap the fuzzer’slinked_unlinkeddimension 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_executabletakinglink: "pool", and a SECOND pool-lane realization insideseed_fleet_entrybeside 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 finalizedLOAD_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 thenbootstrapanswerstrue, 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 valuetruewherebootstrap_artifactsreturns{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/xThe changes are in this branch’s history (reverted inpit-shop/shop_build.cmwith 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_bodiesfires 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_PLANnamesinline_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 liveappears 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’sobserve.jsonlfor a cause should discount it. mcode_lower: unknown labelshould 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.cm—find_call/semantic_afterwalked across a LABEL, soreplace_sitesspliced a jump target out; measured onarchive/qop.cm’sarchive_from_entries, which was IN THE SHIPPED BOOT CART with a jump tostore_bad_key_2544, a label that was not there. Reproduced on BOTH link lanes, unlinked, so it predates every press change). Turning the warning intoml_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.cmlowered by the pre-fix compiler produced a second dangling label (store_field_stone_1195inrun_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_entrynow 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 atvm_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_KILLplus 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, andpit debugreports 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.cinloop_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 openssource/start_plan.c.ctx->vm_call_depthis 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 duringarc/preempt-loops.THE SECOND
pit compileOF A DAEMON SESSION NEVER STARTS ITS ACTOR. Diagnosed 2026-08-05 (arc/runaway). This subsumes both the “cache-hitpit compile linked_planhang” and the “pit compile profilewedges 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.qoprun 3× in one session: all three answer. So it isshop_tools/compile.cespecifically, not “any second start”.- During the hang the DAEMON IS IDLE, not thrashing:
sample <daemon>shows everyactor_runnerthread parked in_pthread_cond_wait, 0% CPU, 78 MB RSS.pit ps,pit why,pit bundleall 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.jsonlfor run 2 shows apit-shop/terminalactor start and couple, then nothing at all for the whole timeout, and NOactor_startforcompile. Run 1’scompileactor 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’shydrate_cached_executablewalked the executable’s module list by RECURSION —next -> hydrate_mach -> build_read(cb) -> hydrate_mcode -> build_read(cb) -> next— andbuild_readis fully synchronous (shop_store.cm:503reads 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.cehas 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 throughparallelover 98 deps, entry decode) — the two together crossedACTOR_STACK_LIMIT(1024,source/pit_internal.h:2218) partway down the list. The raise killed the clerk’s TURN with the shop’srealize_startrequestor unresolved: no actor, no reply, no log, clerk still alive. Instrumented proof: the last thing the clerk emitted washyd next i=63of 79 modules; the same hydration from the cold-build path completed all 79. The “only compile” specificity was module COUNT —bundleis small enough to fit. Fix: the module walk is now a pump, so re-enteringnextwhile 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 bytests/warm_start_repeat.ce(baseline suite). E3’sPLAN_IMAGE_MAXwas never it, as recorded.
pit cement --appsat at 0% CPU for 19 min — SAME DEFECT, closed by the same fix:cementstarts 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_blobswas the same shape — FIXED 2026-08-06, same arc. Found while fixing the above: it walked the same module list with the samenext -> build_write(cb) -> nextrecursion, andbuild_writeis 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_payloadswalks 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.cmclosure_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-pressmake seedreportsheap_peak_bytesexactly 8589934592 — B4’s 8 GiBACTOR_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 insidecatalog_put_value’s disruption arm, which surfaces as the bare, causelessmach 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 themember_memo_keycrash 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, themember_memo_keyclerk death. It was a null concatenation, not a memory runaway.shop_fetch.cmmember_memo_key(added bydd4026860, the session closure memo) spelled its key withtext(owner_pkg).owner_pkgis NULL for a member that belongs to no package:package_from_locatorcannot attribute an absolute host path (shop_source.cm:107— the first/-split part is""), and the only fallback is the request’spackage, which a publish does not carry.text(null)is null, so the concat raisedcannot apply '+': tried to add text to null— inside thefile()callback, where the scan’s disruption arm cannot see it andparallelswallows a requestor raise. The clerk died mid-walk; callers sawtarget stopped; and because the clerk is not respawned, the daemon then answeredtarget is goneto 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 ownactor_deathrecord for the failing run readspeak heap 32768KB, final 16384KB held / 2042KB live— 32 MB, three orders off a gigabyte — and apanicrecord 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 atmember_memo_keyand passes with the fix. Regression test:tests/store_freshness.cepackageless_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:387spells"missing executable '" + text(item.target), which would raise the identicaladd text to nullon 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 reportedclosure incomplete for …: 6 missing {mcode_metadata blake2:e71b82a8… (module lang-endowments/log) <- …}andcli/{bundle_info,ship_app_bundle}went red on the merged tip while passing onarc/runawayalone. The walk now asks the same questionshop_store.mcode_unit_metadataasks — 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 sourceis advertised but neither implemented nor refused.shop_tools/publish.ce:19offers “mcode (default) | source” and the retirement message atshop_realize.cm:1931points callers atsource, butrealize_payload_mode(shop_realize.cm:332-334) accepts only mcode/mach/mixed, so--payload sourcesilently 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.cedoes not compile in-process —compiler.mcode_unit_resultreturns null for it intools/builder_diet_probe.ce, and./pit compile mcode shop_tools/mcp.ceanswersfile not foundfor a file that is on disk. PRE-EXISTING and not a merge effect: identical failure atarc/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 SECONDpit product/pit cementin 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 inshop_realize.cm hydrate_cached_executableoverflowingACTOR_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 “importspit-shop::shop_buildinto 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 afetch:line — and the client waits forever../pit downbetween runs is the workaround, and it is whytests/cli/product.cemakes exactly onepit productcall. Same signature as thepit compileentry 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.cmfind_realization/load_cached_executable,shop_build.cm:474 embed_executable_cached_payloads), and it is NOT the E3 scratch-image cap. The four sightings arepit compile parse,pit compile profile/linked_plan,pit cement --app, and nowpit product/pit cement --product; every one of them importspit-shop::shop_buildinto a CLI actor, which is the shared shape. Fixing it once should close all four. It makespit producta once-per-daemon command today — survivable for a Makefile line, not for a loop.OPEN, AND IT CONTRADICTS THE CLUSTER CLOSURE ABOVE:
pit compile profileSTILL WEDGES WITH THE WARM-START FIX MERGED. Measured 2026-08-06 onarc/integrate4at967cac5d8— i.e. witharc/warm-start’s hydrator pump in, and with its owntests/warm_start_repeat.cePASSING in the same gate run.pit --quiet --machine compile profile pit-compiler/fold.cm, thebench.stageslane ofperf/run_bench.py, ran 9 h 09 m without answering and had to be killed.arc/warm-startwas 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 benchcannot complete;perf/run_bench.py --skip-stagesgets the other two lanes. Not investigated — recorded on sight, budget spent landing the merge instead. NOT REPRODUCIBLE ATdev@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 fullperf/run_bench.pywith NO--skip-stages, which completed all three lanes and reported273 metrics … ok=True,failed: 0. Somake benchcompletes and thestages.*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 inembed_executable_cached_payloads → decode_artifactat 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 at967cac5d8and are present at32f03fdc4. 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 thepit debug sitesleak 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.celeaked 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 apthread_joininexit_handler” (plans/todo.md’s entry, now corrected): lldb on a live survivor showed thread #1 inmain_backend_run → CFRunLoopRun— it had never entered teardown — withscheduler_is_shutting_down() == 0,pit_is_daemonized() == 1, andscheduler_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 downprinted “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 ofactor_free, the actor destructor (source/scheduler.c,if (was_registered && scheduler_actor_count() == 0) scheduler_shutdown();). But the live count drops earlier and elsewhere, inactor_halt’sshdel(actors, actor->id), andactor_freeis 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 endsHALT … debug_tool_worker count=0/FREE was_reg=1 count=0(shutdown fires); the leaking run endsHALT … debug_tool_worker count=0with noFREEat all. Fix:scheduler_note_actor_gone()asks the question wherever an actor leaves the registry —actor_haltas well asactor_free,scheduler_shutdownbeing idempotent — and countsstarting_actorsalongsideactorsso 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 sodownlands mid-turn,down. Before: leaked every time, anddownreportedtimed 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, andcli/debug_toolruns 3× with ZERO survivors (previously one per run). Pinned bytests/cli/daemon_exit.ce(added toCLI_TESTS). This also delivers half of the deferred ruling “a daemon whose listener halts should exit” inplans/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_tooldoes 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 run —
pit_boot_trace_setup(paths.shop, !daemonized)arms the stderr mirror for every non-daemonized boot, so the shippedmcodebinary prints fourboot:/[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 failures —
log.*is a silent no-op with no logger actor (lang/logging.cm:78, “if (!log_target_id) return”), somcode’s “cannot read foo.cm” reaches nobody and only the exit code carries it.pit product --logger logger/sinkships 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.
- 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. - 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. - 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, ...)inpit_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 inlogging.cm’s own comment (“C’s boot sink covers that window”):pit_boot_tracewrites to stderr from C with no actor involved. Closing the gap is (a) an endowment or intrinsic that letslogging.cm’sif (!log_target_id) returnbranch 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.- The build-time contradiction (
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_LIMITis 8 GiB. (b) The seed press held every fleet member’s decoded payloads at once —seed_fleet_entryhydrated 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-decodestreams it: a resident row travels dehydrated andboot_cart.press_resident_rowshydrates, presses and releases one member at a time, andpool_executabledrops the source units the moment the link has taken them.make seedreaches “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, underpool_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 lintis red ondev— 8 platform-include violations, all ininternal/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 bybf713e39e(“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 cleandevcheckout at8791b2c91before any lane merged. The fix is either moving the code intoplatform/<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 cleandevcheckout at8791b2c91, beforearc/debuggermerged, 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 expectsspinfolded intomiddle, but streamline now lowers that edge as a tail call (middle→tail_call:0), sospinandmiddleare separate frames and no inline note can namespin. 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 lintred ondev, 8 platform-include violations ininternal/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 wholebootstrap_*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) calledopen/read/write/stat/opendir/mkdir/renamedirectly, with#ifdef _WIN32arms for mingw’s one-argument_mkdir, drive -letter roots, backslash separators,MoveFileExA, andO_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 seamsource/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, somake manifestwas the only wiring.internal/os_posix.cnow names no OS header at all and carries a guard comment saying so. Lint readsOK (8 allowlisted core files, no new leakage)— the allowlist did not grow. Diagnostic quality was deliberately kept, not traded: every seam call that had astrerrorin its message still has one, threaded through caller-ownederr/errlenbuffers 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_valueswallowed B4’s named heap-limit refusal. The disruption arm setok = nulland said nothing, soACTOR_MEMORY_LIMITrefusing by name surfaced one layer up as a baremach cache write failed(shop_build.cm:967). The reason itself CANNOT be recovered in the arm and never will be:disruptis a bare keyword carrying no value, and the VM deliberately clearsdisrupt_msgthe instant a handler takes it (source/mach_vm.c— “a caught disruption leaves nothing behind”), because leaving it set made the next baredisruptreport the earlier, caught failure instead of itself. So the fix supplies the JOIN rather than the reason: anote_write_disruptionhelper 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 inshop_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:410open_store’screate()arm setsstore_opened = falseand discards; a shop that cannot make its own directories reports only “false” to its caller. Beyond this file, the arms worth auditing next are inshop_build.cmandshop_fetch.cm, where themember_memo_keynull-concat already hid inside afile()callback for weeks. The general lesson is now twice-proven: in a language whose disruptions carry no value, a silentdisruptionarm is an unbounded information loss, and the only defence is that every arm SAYS which operation it just ate.mach_pool.caccess handler still refuses record operands — the C-side follow-up to B1’s spelling resolution;mach_press.resolved_coderesolves 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 carriesprofile: "ship"as a name.arc/module-exportshas landed, so the file is free; the retirement was attempted in that batch and deliberately deferred, because it is bigger than a name swap.profileis a REQUIRED field ofpit.mcode.link-plan@1(mcode_link.cm:82rejects 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 attests/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:dev→ship(passflags.cm:208) andboot→ship(mcode_link.cm:2180) make the selection constant today.
Premises refuted by the work these plans ordered
work-proposal.mdD2 /programs.mdphase 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 inpit-shop/clerk:clerk.ce:833handles thebootstrapmessage by callingshop_build.bootstrap_artifacts(shop_build.cm:3104), which is what reachesboot_cart.cement_root_cart(shop_build.cm:3193) and through itboot_cart.pool_executable(boot_cart.cm:296,309). The +292 KB was therefore never attributable toshop_actor’s closure. pit-shop/builder_workeris itself a cart executable. It is a member ofDEVELOPMENT_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 ofDEVELOPMENT_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.
- The boot press does not run in
Suspended coverage — owned by a named lane
tests/cli/app_bundle.ceandtests/cli/aot_app_bundle.ceare SUSPENDED by D1 (R1); D2 (pit product) owns un-suspending them. Both asserted the app-mode ENVIRONMENT PROBE:pit bundle appcopiesdist-vm/pitintoContents/MacOS, and the launcher inferred “this is an app — boot it in-process with its own shop” from the.apppath. R1 deletes that inference, so a copied binary now carries the DEVELOPMENT cart’s declaration (resident/shop/daemon) and the.appbehaves as./pitdoes. Nothing in those files can fix it:pit bundle apphas to FORGE its binary around a cart that declares one-shot, the waypit cement --app+pit forge --bootalready do for a single executable — and no cement mode presses the dev FLEET as a declaring one-shot yet. Un-suspending = deleteR1_SUSPENDEDin 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 theerrorslist, 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_resultsent immediately before$stop(). Atests/cli/*.ceactor that sends its own{type: "test_result", ...}and then stops has the message lost — measured directly: a probe sendingpassed: 1, total: 1still summarised aspassed: 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 undertests/cli/is currently invisible.skipped: truerenders exactly like a file with no tests, and theerrorslist that would have printed the reason (summary_viewrenders errors whateverfailedis, so a skip notice could be both visible AND green) never arrives. Not fixed here:shop_tools/test.ceis 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 renderskippedexplicitly.
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, forperf/ir_census.pyandperf/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’sbenchblock, 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.cmcarries one switch per optimization andtests/passflags.cmproves 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 inplans/proposal-notes/r5-stone-drop.md. Practical rule: after editingpit-compiler/,./pit downre-realizes it for scripts, but onlymake seedmakes 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-3967never 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_taxonomyreports 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.cmMETHOD_STORE/METHOD_DEFLATE, a65535inpit-compiler/compiler.cm) — the census recovered a value the declaration does not carry.ok: trueandlink_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.ceandtests/recipes.ce— “compile failed”.network_infoFIXED (arc/gate-hygiene, 2026-08-06);recipesfixed separately onarc/feature-stripand deliberately untouched here. Both had the SAME cause, and the compiler was right in both:lengthis a sensory intrinsic (pit-compiler/mcode.cmsensory_ops) and a sensory intrinsic inside alog.*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:441asserts that refusal deliberately. The fix is to take the count outside the log call. The same latent break is still spelled intests/step_debug.ceandtests/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— barepsresolves asform: 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 reported4 passed / 1 failed / 5 total, and aftermake smokereseeded it reports21 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_resolvewas red, the aggregate./pit testprintedfailed: 2but 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".efabb7b11fixed 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-keyedfound_bcould never be set, and the two-slotsitesaccumulator 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.cesort_by_size_descwas an O(n²) selection sort run over EVERY object in the snapshot, while both callers slice to theirlimitimmediately after — so the whole tail was work nobody could observe. The test dumps thepit-shop/policyactor, 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-facingpit heap <actor> summaryfrom 5.6 s to negligible.heapno longer appears in the ten slowest tests.scripts/check_compiler_tooling.sh— “fresh source press no longer matches the fused generation”. The assertion pinsruntime/tests/fixtures/test_good.cmat 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 meansmake 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’spit-compiler/as well as on the diet branch (4+ minutes each, 0% CPU). It drives repeatedcompilecommands through onepit shell --machinesession, which is the same shape as the “pit compile profilewedges 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 = 2over 49 units means ~25compilestarts 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_percentALONE, 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.cmwas 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:
| unit | instructions | source lines |
|---|---|---|
pit-compiler/streamline.cm | 93505 -> 101892 (+9.0%) | 4994 -> 5482 (+9.8%) |
pit-compiler/mcode.cm | 33450 -> 35907 (+7.3%) | 4619 -> 4869 (+5.4%) |
pit-compiler/inline.cm | 16941 -> 18602 (+9.8%) | 900 -> 989 (+9.9%) |
pit-compiler/compiler.cm | 22204 -> 23831 (+7.3%) | 1717 -> 1891 (+10.1%) |
shoplib/mach_press.cm | 1009 -> 2385 (+136%) | 143 -> 251 (+76%) |
pit-compiler/numeric_literals.cm | 254 -> 1312 (+417%) | 42 -> 147 (+250%) |
pit-compiler/panic_outline.cm | 5709 -> 6278 (+10.0%) | 295 -> 345 (+17%) |
shoplib/debug_resolve.cm | 11291 -> 11766 (+4.2%) | 700 -> 751 (+7.3%) |
pit-compiler/fold.cm | 16135 -> 16584 (+2.8%) | 1094 -> 1180 (+7.9%) |
pit-compiler/callresolve.cm | 15799 -> 16183 (+2.4%) | 767 -> 799 (+4.2%) |
pit-compiler/tokenize.cm | 8803 -> 9087 (+3.2%) | 528 -> 562 (+6.4%) |
shoplib/actor_find.cm | 955 -> 976 (+2.2%) | 128 -> 137 (+7.0%) |
pit-compiler/structured.cm | 27359 -> 27381 (+0.1%) | 1113 -> 1115 (+0.2%) |
pit-compiler/parse.cm | 27186 -> 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_codetomach_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 fornumeric_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_percenthair-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_TOLERANCEinperf/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.jsonno longer carriesstages.*ceilings — 83 rows, not 114.pit compile profilewedges 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 fullperf/run_bench.pynow fails the gate with 31unrecorded metriclines until that hang is fixed and the baseline is re-captured without--skip-stages. That message is the ask.tests/freeze_module_loop.ceis flaky under full-suite load — new witharc/preempt-loops, green alone and green in two of three aggregate runs,disruptedin 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.cereported4 passed / 1 failed / 5 totaland the aggregate./pit testwas2398/2then2399/1— with, again, one failure counted and unnamed. Amake smokecleared it:shop_resolvewent to21/0/21and./pit testto 2416 passed / 0 failed / 2416 total. Note the aggregate total moved 2400 -> 2416, which is the same tell one level up.make seedis 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. Runmake smokeaftermake seedbefore believing any suite count. shop_resolveSETTLED at thearc/integrate-budgetmerge (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 thatmake smokecleared it, one thatmake smokedid NOT and the failure survived with its own changes stashed. At the merged tip (arc/retire-budgetinto dev@162d10de5), after a cleanmake smokeon a daemon brought down and confirmed gone by socket path,tests/shop_resolve.ceis 21/0/21 on three consecutive runs, and the aggregate is 2426/0/2426.make nukewas 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 andfail()callsfinish(), so the first failing step ENDS THE RUN.totalis therefore how far it got, not how many tests exist — there are always 21step(...)calls in the file.4 passed / 1 failed / 5 totalmeans step 5 failed, and step 5 is"shop recipe program roots provide bare commands"(line 191), which is exactly thebare ps resolves as form: mountedsymptom 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’sfailed: 2printed 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 finalizedLOAD_MODULE_RESULT) and theresident_executablethe 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 answerstruewith exit 1, writes no artifact and presses no resident, with no named refusal anywhere, reproduced on three seeds. That missing diagnostic incement_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
truewith exit 1 and no named refusal” mystery was a FLATTENING, not a missing diagnostic.pit-shop/clerk.ce’sbootstraphandler wrotereply_failure’s RECORD ({ok, error, problem, reason}) straight intoerror, andtext(record)is “true”. The refusal was always there; one hop before the client it became a boolean. Fixed to flatten withshop_util.problem_text/problem_diagnostic_messages, and the whole tail fromboot_cart.cement_root_cartto the bootstrap client now names its stage and its member: each of the four presses is guarded,press_resident_rowssays “root cart resident 6/12 ‘pit-shop/courier’”,bootstrap_artifactskeeps a belt around the synchronous cement, andshop_tools/bootstrap.cerefuses a non-record reply by name instead of forwarding it. link.missing_memberwas a wrong refusal and brokepit cement --appTODAY, at dev tip, with no other change in the tree.mcode_linkfailed 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, andnet/net.cmuses that as a feature test (is_function(net_impl.provider) ? net_impl.provider() : ..., wherenet/net_darwin.cmexports{config}and noprovider). So every press of any program reaching net.cm refused, twice../pit cement --app pit-shop/courierreproduced it in one command. The site now keeps its guarded load and counts as generic;tests/mcode_link.cmpins 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 unitpresses that program into 30 separate pools and self-verifies. The emitter’s covering carries{unit: <plan slot>}per import andMACH_LOAD_MODULE_RESULTreadsresults[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 byK_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 bystable_binding_key) does not depend on the enclosing plan — andsource/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.cplan_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 invalues[]) beforeresident_functionchecks 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 inshop_build.prepare_simple_bundle(plus the two otherlinkreads),context.link/build_msg.linkconstant inshop_realize,mcode_cache_keyfolding the constant “pool” (keep it — dropping it collides with a pre-R7 shop’s static entries). The seed then fails at press 2 withmach_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_callsruns an identical instruction stream at 8.76 ns/i on the pool lane vs ~1.9 on the deleted register lane; isolated tobind_getup. Start:make benchshows the row —kernels.closure_calls.*inperf/trend/latest.json, frombenchmarks/bench_suite.ce. The lane ismach_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_callshas been seen at 16.4 ms and 4.0 ms on the same 1.8 M-instruction stream). The gatedkernels.closure_calls.instructionsceiling 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 infoonboot/root.cartfor 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 apit-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=Nis 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_joininexit_handler), surviving their parent’s kill. Not yet deterministic. Start:tests/cli/debug_tool.ceruns, then lldb any 0%-CPU survivor atexit_handlerand find what thread it joins. Two sightings are already recorded above — thepit debug sitesworker leak and the strandedpit forgeon the worktree socket — and a third landed 2026-08-07 during this very cleanup (amakethat exited 0 still left apit forge darwin --out build --boot boot/root.cartholding.pit/pit.sock, so the next./pitrefused 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.mdnames recipes that do not exist (macos_arm64/macos_x86_64/linux_arm64, 5 sites, verified still present 2026-08-07); the real names aredarwin/linux/etc. Mechanical fix.- The word “lane” does double duty — the rulings’ “one execution lane”
(
docs/spec/mcode.md:14) vs the compilation lanes atmcode.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 fromdocs/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_onlyshape). The guard-comment sweep rule lives inplans/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.cmpins 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.cepins 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.
- trueerror rendering. A failing test with a long reason renders as bare- truein 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 stalePIT_POOL_PAYLOAD=1state block — already deleted. The file’s surviving comment is a legitimate reseed warning.- Stale “pit.cart@1 trailer” comments at
source/cart_boot.candcake/manifest.cm— already fixed; grep finds nopit.cart@1at either site. - The
allow_compilecontradiction inpit-shop/policy.ce— already resolved;allow_compiledoes not appear in that file. The diary’s “deleted 2026-07-22” record was right. - The storage revisit (
boot/contentgrew to ~86 MB) — dead as written. There is noboot/contentdirectory; the boot tree isroot.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.mdis the live queued arc for it.
Found live that the inventory called dead
Pit_MachPoolInstructionis LIVE. The L4 dead-code inventory listed it for deletion; it is called atsource/mach_pool.c:1576and:2005and declared atsource/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-stashis deleted. Its tip was5984b11a12c7565a45c73d726d4064add8f303bf(“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.cmis 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_fileat:2429,2431), plustests/pgo_store.cm,tests/pgo_executable.ce,tests/cli/pgo_store_atomic.ceandtests/fixtures/pgo_store_writer.ce. L2b was about collection. Likewisepit-compiler/pgo.cm(used bypgo_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-617andsource/mach_pool_test.call 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_collectrefusal andpit-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 psthatmake smokeends with mounting the program. After./pit down(socket confirmed gone), on a daemon that had never runps,./pit test run tests/shop_resolve.cewas 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.chad no<limits.h>— FIXED onarc/ci-postpush.wota.honly pulls<limits.h>inside its#else /* PIT_WOTA_BITS == 64 */arm (line 184, nesting depth 2 under the#ifat line 41), so on any 32-bit-wota profile the translation unit that usesINT_MAXat line 428 sees no declaration. gcc refuses; Apple’s libc leaksINT_MAXthrough<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 ischeck-profiles’nan32,fixed32andnan32-compactarms, i.e. the playdate and console rungs.scripts/check_playdate_api_coverage.pychased a moved file — FIXED.main_playdate.cwent undersource/in01bba7317(2026-08-02) and the script was not followed;playdate-simdied on a bareFileNotFoundErrortraceback. Nowsource/main_playdate.c, and a missing entry is a named refusal listing every stale path in one pass.scripts/check_compiler_tooling.shneededxxd— FIXED. ubuntu 24.04 splitxxdout ofvim-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 headertwice) — the actual cause was one line above,xxd: not found. awk now emits the bytes itself underLC_ALL=C; verified byte-identical to thexxdoutput (3480 bytes) before the swap. The image still has noxxd; 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:
web—check-web: target seed FAILEDafter 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.windows—target 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, allmcode_metadataforlang-endowments/*under one parent digestblake2: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 jobs —cross-run-linuxgot as far asforge: tests 18 passedtwice and thenforge: candidate bootstrap failed/compile failed pit-shop/daemon_transport.cm: target is gone, dying inmake smoke’s seed.target is goneduring 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_policyfails on every 32-bit-value arm.numrep-nan32,numrep-fixed32,nan32+compact,nan32+kim8all exit 1;fixed32+kim8+compactexits 134 (SIGABRT). Every 64-bit arm is green, includingvalidate-gcandforce-gc. The macOS runner is bare metal — no container, no image — so this is real and it is not new; it is newly visible, because6c68f0a47promoted macOS to gate dev pushes andmake checknever runscheck-armsat all (CLAUDE.md says so explicitly). Diagnosis pointer:source/gc_policy_test.cgates its block-sizing cases ongc=compactonly (thecopy_policyflag, ~line 665) and has no gate on value width, while the 32-bit arms also carryPIT_MAX_IMAGES=2and 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.bench—bench.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.pyshould 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:
source/pit_profile.h:204—PIT_PROFILE_STRINGisname;value;rep;endian;text;record;gc;wota. No system, no arch. Verified by reading the stamps out of both committed seeds: the Darwinboot/root.cartand the Linuxboot/targets/linux/root.cartare byte-identical atCART_OFF_PROFILE_STAMP(host-nan64-v1;value=64;rep=nan64;endian=le;text=utf32;record=hash;gc=copy;wota=64).source/cart_boot.c:206— the stamp compare isif (!trusted && ...), andcart_mount_root_image(source/cart_boot.c:311) passestrusted=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:570checks only thatboot/root.cartis 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:302 —
cannot 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