Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.

The next arc — two streams, dispatch-ready briefs

Ruled by John 2026-07-26/27. Stream 1 is the recipe arc (target knowledge + meson removal); stream 2 is phase C. Both approved to run in parallel; each stream is serialized internally. The design they implement is plans/edicts.md “The build products and the recipe” — John confirmed the full articulation and said everything in future phases hangs off it.

The compiler WAITS — REFINED (John, 2026-07-27 evening): the wait is about PERFORMANCE. “I don’t care about performance regressions right now because the runtime is changing so much.” Do not chase compile-time numbers, the record_lookup bisect, log-stripping, shared-tail, or shapes-emitter arcs. Compiler work that makes its outputs fit the new lowering/linking model is FINE and probably needed: the import op emission (when the production import lane lands), the record-literal provenance fix (origin-map model fit), and honest diagnostics (the live-function slot-limit message — John’s read: almost certainly the 255-slot frame limit hit silently, seen before). The hpack miscompile stays waiting (correctness bug-hunt, neither category) unless John says otherwise.


Stream 1 — the shape, as confirmed 2026-07-27

Landing 1 (recipe file) → landing 2 (the 39 cake sites + meson + scripts read recipes; no code knows what a target name means) → landing 3 (the runtime builder — the NEW tool replacing meson: reads the recipe, derives the TU list from platform package + lists, drives cake per-TU, links, renders the static table from shoplib::symbols) → landing 4 (meson deletes; check-arms and the build scripts drive the runtime builder; the extension-spec lane hangs off the builder). Briefs for 3 and 4 get drafted as their predecessors land; the builder’s open design questions (implementation language, bootstrap.sh relationship) go to John before landing 3 dispatches.

Brief 1 — stream 1, landing 1: the recipe is a file — DONE (4fb02c57 + reseed 8fd0fab9)

Landed 2026-07-27, all gates re-verified green at merge. recipes/<name>.json (12 targets), shoplib/recipe.cm the ONE reader (validation: groups never mix, platform constraints win, paging-fact/budget coherence), fingerprint_input() folded into recipe_fingerprint, platform packages carry requirements/constraints, drift gate in tests/recipes.ce proves recipes agree with cake’s live tables until landing 2 deletes them, docs page docs/architecture/recipes.md. daemon-fresh now also restarts on recipes newer than the socket (loader caches per daemon session).

John ruled on the schema calls and contradictions, 2026-07-27 (edicts, “Three refinements”; the trim landed same day):

  1. JSON ratified.
  2. Platform names FLATTEN IN LANDING 2 (n64, not platform/n64).
  3. sched stays in the stamp because the profile string carries it; whether it should is an open checkable question — evicting it later is one reseed.
  4. The recipe describes the runtime only. payload and app_fleet were cart/bundle knowledge wrongly transcribed into the recipe — REMOVED from the schema, the twelve recipes, and the drift gate. Payload form and the service fleet are per-build inputs to the cart/bundle description; the common choices live in committed build descriptions (landing 2 relocates payload_policy()/app_fleet() there, not into recipes). The playdate source-vs-mach contradiction and the n64 fleet oversight both dissolve — they were per-target answers to questions that are per-build.
  5. No permission lists anywhere — the toolchain is the gate. constraints blocks deleted from platform manifests; the loader validates shape, not possibility; recipes are suggestions. The paging “fact” went with them (windowing follows the rep, not the platform). Platform requirements = names and build data (entry, providers) only.

Still open: c_native: [] = everyone is codified in the loader; edicts wants dev blessing eventually explicit rather than empty-means-all.

Landing 2 spec — the target knowledge that did NOT fit the recipe (agent census, exhaustive)

Added by the 2026-07-27 rulings: (a) flatten the platform package names — the platform/<os>/ directories become flat bare-named packages (n64, apple-family, …) and every recipe’s platform field updates; needs the directory move or the nested-root resolver fix, whichever is simpler. (b) payload_policy() and app_fleet() relocate into per-build cart/bundle descriptions, not into recipes.

Everything below either converts to read recipes/platform-package data, or is named as a deliberate non-lever. In cake (cake/plan.cm): link_flags/link_flags_cold/ WGPU_LINK_TOKENS (per-target framework+lib lists — probably platform-package requirements.link); platform_runtime() (11-branch per-target C list; recipe only carries the daemon bit); base_runtime/support_runtime/gc_runtime+gc_arms/ text_runtime+text_arms (file seams the stamp axes select but the recipe does not name); static_extensions() lead rows (emscripten → internal/os+internal/sysinfo; five consoles → internal/os) + the internal/couple trail row; floor_extensions(); MANIFEST_SYSTEMS, PLATFORM_MANIFEST_DIRS, LINKED_C_PACKAGES (last two now exported solely for the drift gate), ROOT_MODULE_DIRS, ROOT_PACKAGE; native_index()’s PLATFORM_MANIFEST_DIRS walk.

In meson.build: numrep → -DPIT_PROFILE_* branch + object_abi_nan32; gc/text macro branches; per-system framework dependency() blocks; -DTARGET_APPLE_EMBEDDED/_IOS/ _TVOS/_WATCHOS; -D_WIN32_WINNT + 18-library windows link list; -DTARGET_PLAYDATE; native_payload_supported; main_source selection; exe_ext/rpath/-rdynamic vs -export_dynamic; -DPIT_TARGET_* facts; extensions_spec trimming; PIT_HAVE_HOOK.

In scripts: region budgets live in two incompatible places (only n64’s harness lane passes PIT_CAPACITY_*Pit_NewRuntimeWithConfig; fleet lanes get them from pit_profile.h because pit_init() zeroes the config and PitBootConfig has no region fields — recipe records the numbers, nothing reads them yet); PIT_MAX_IMAGES set by no script, only profile arms, asserted ==2 by the two abi tests; entry points split three ways (playdate manifest row / dc-psp-n64-ps1-web hardcoded compiles / desktop main_backend_* + source/main.c); target-only sources outside every manifest (device_runtime_playdate.c, libm_ps1.c, platform/ps1/include/*); toolchain accommodations (-include limits.h, -D__thread=, -Dalloca=, -DMINIZ_NO_*, -G0, -DIO_BACKEND_SYNC, -DPIT_PLATFORM_*); cross-toolchain env (KOS_BASE, PSPDEV, N64_INST, PSN00BSDK_PREFIX, PLAYDATE_SDK_PATH, emsdk python, mingw prefix, xcrun triples + min-OS); payload/lane selection is a second dimension (PIT_*_PAYLOAD, PIT_PS1_LANE — changes obj dir, artifact name, cart embedding, and on PS1 the numrep itself); --profile-name hardcoded in all four *_cart.sh and can silently disagree with the compiled runtime; emulator/run config (no lever character); packaging metadata (pdxinfo, Info.plist, n64tool/mksfoex titles); --minimal-content inconsistency (web/windows seeds skip it); the static|lead→modules→static|trail ordering + probe-tag filter reimplemented identically in seven manifest-reading scripts; text is a dead lever on every cross target (all seven loops hardcode utf32).


Brief 1 (original text, for the record)

You are working on the pit language runtime. FIRST: git log --oneline -1 — verify your base against claude/rebuild-from-green and git reset --hard if stale. Run plain make FIRST or ./pit resolves to the main checkout’s daemon. Read plans/edicts.md in full — “The build products and the recipe” is the design you are implementing — and plans/roadmap.md’s resolution-arc section for the crossing recipe, the make nuke lesson, and the boot/boot.cart wedge remedy. DO NOT COMMIT. ./pit down when done.

The recipe becomes a real, committed file. Today “target” is 39 sites in cake/plan.cm, a PLATFORM_MANIFEST_DIRS table, and the pit_profile.h chain. The ruling: a target is a NAMED RECIPE — a platform package + the free levers + the package lists — and the platform package declares its own requirements (startup file, scheduler, paging facts) and constraints (levers it forbids).

This landing: (1) define the recipe schema — two lever groups that never mix (stamp-entering: rep/text/gc-ABI/endian/wota; non-stamp: budgets, scheduler arrangement, linked packages, endowment lists + whole, capability levers) plus the platform-package reference and the shop lists; (2) a recipes/ directory holding the standard recipes, derived faithfully from today’s pit_profile.h arms + PLATFORM_MANIFEST_DIRS + per-target script knowledge — same content, new home; (3) a loader the shop and cake both read, folded into recipe_fingerprint; (4) the docs page that is currently the largest admitted gap: what a recipe is, where it lives, how a build selects one (docs/architecture/targets.md points at it). Do NOT convert the 39 cake sites yet — that is landing 2; this landing makes the object they will read. If schema questions arise that edicts does not answer, STOP AND ASK — the format is John’s.

Gates: make check ALL GREEN, check-arms 14/14, check-cli ALL GREEN, lint, manifest idempotent, budget STAYS green. Report: the schema with an example recipe verbatim; what today’s target knowledge did NOT fit the schema (that list is landing 2’s spec); everything in this brief that was wrong, proven.

2026-07-27 — the stream-2 design is RATIFIED (do not relitigate)

John stated the full artifact/shop model back and it was checked against the tree. Verdict: docs/spec/artifact-formats.md and its neighbor pages ARE his description; stream 2 is the walk across the built/unbuilt line. The load-bearing points, so nobody re-argues them:

  • Store contract: manifest-first, per-object by content hash (get_manifest / get_content(hash, range?)); git hosts, plain folders, mounted shops, bundles, and carts are all backends. KNOWN GAP: the git lane materializes whole repos today; per-object fetch is the target (real design work, unscheduled — needs a source-side manifest without a full clone).
  • One selection per package: the edicts package-identity invariant + aliases as version pins + lock.json + the executable manifest’s one-identity-arm package rows. Specced in full; ENFORCEMENT lands with the executable manifest (stream 2, after mcode@3).
  • The names→hashes→indices gradient: locators live at catalogs and manifests; content hashes name whole artifacts in the store; inside an artifact there are only dense indices and self-relative offsets. A pool does not know which locator a function came from — the executable manifest is the border table. Pools carry stable mcode function IDs for MATCHING (hot reload, PGO), never for lookup.
  • N mcode → 1 pool is the lowering+linking step (mach_pool_emit.cm, Pit, unbuilt, blocked on mcode@3). The link plan is the pool-granularity lever and therefore the hot-reload granularity lever. Pools are mapped and read in place: “no per-load or per-image materialized constant/object tree is permitted.”
  • Cart = sealed store + boot rows + placement plan (banking, XIP, fast-RAM hints); bundle = the same store snapshot, portable, no placement. “Small boot + a wad of hashes” is the design as written. The cart survives fusion because placement is physical and boot precedes any shop.
  • No second kind of mach. The dev/ship split is the cart WINDOW: pmcd (mcode, no stamp, lowered by the mounting runtime — survives ABI changes) vs a finalized pool (stamped, mapped). mcode is the portable artifact. The munging kill-list stands with its ordering constraints: the per-unit pmac transitional reader; mach_materialize_cpool
    • the rt->ct_* intern table + code-cache constant ownership (one coupled deletion, AFTER constants live in mapped images); pre-image shape materialization; pit.executable@1.
  • Debug and PGO ride content hashes end to end: pit.mach.origin@1 keyed by pool hash → {mcode hash, function/site ID, source hash, span}; mcode carries spans; PGO observations in mcode identity so profiles survive recompilation; bundle debug slices carry origin maps + source trees. The origin map lands WITH mach_pool_emit, not after.
  • Tools in Pit: the cart codec already is (cart_v2.cm/cement.cm); the pool emitter will be. C keeps the VM interpreter, the pool validator/mapper, the fixed cold-boot header (the versioning boundary), and the one bootstrap mcode-lowering lane a bare runtime needs before any fleet exists.

Dispatched same day, in parallel: stream 1 landing 2 (cake reads recipes; flatten platform names; payload/fleet → build descriptions), stream 2 landing 1 (mcode@3), and a docs alignment sweep enforcing all of the above across the site.

THE OVERNIGHT RUN (2026-07-28 → 29) — what landed while John was away

Every landing below is merged green on claude/rebuild-from-green with the full battery re-run by the orchestrator. Judgement calls made without a ruling are in plans/overnight-decisions.md (D1-D10); the stone 6/7 execution map is plans/stone-6-7-map.md.

  • fecf1e48 the git requestor test cloned the repository. config.runtime_path as a clone source handed git a 1.07 GiB pack on every test all; pack-objects pinned a core per run and leaked a ~50MB clone each time (2.0 GB accumulated). John spotted the CPU himself. A one-commit fixture: 1m+ and 51MB → 0.84s and nothing.
  • eabbb23a cli/terminal was never a flake — it was a product hang. pit shell decided a target was alive from a point-in-time registry probe, so an actor that replies and $stop()s in the same turn stayed attached; the next line went to a dead token, and a send to a dead actor never calls back — no reply, no reason, no deadline. Reproduced 7/10 standalone, 0 hangs in 70 runs after, negative control returns it at 13/20. The leaked pit shell we had recorded as an “aggravator” was this hang’s symptom.
  • 29412843 the forge object cache could serve a partial write (direct-to-key writes, existence-only probe — one Ctrl-C poisons a store shared by every worktree), a stale probe verdict (a cache hit counted as “libgit2 present”), and another compiler’s output (the resolved cc and build-relevant env were folded nowhere). Receipts, never-cached probes, and a compiler-identity fold. The manifest staleness stamp became pit cake manifest --check — it folds every input by construction because it is the thing that reads them, at 50ms warm.
  • f2b74681 one declaration says what a key folds, and a gate proves it complete. This bug class had hit six times in one arc. shoplib/key_inputs.cm is now the single published declaration read by both the producer and the test oracle. Newly folded: mcode_lower.cm (shapes every mach payload), canonical.cm (decides every stored record’s bytes), and the six C files that are the mach generator — a codegen fix no longer needs a remembered PIT_MACH_ABI bump to invalidate anything. stamp_dir stopped folding an absolute path, so a worktree now shares compiled units with its parent checkout. The gate would have gone red at the commit introducing each of the four recent instances.
  • f91ba2fb an image function keeps its locals. The frame trim read a register-only shape (NULL for images ⇒ “retain nothing”), so every module body run from a pool had its top-level variables wiped on return — logging silently dropped every line, letter handlers read null callees. Plus mach_exec_entry_line’s image arm passed libc’s index as a constant-pool index: a SIGBUS whenever the hook was armed, and it compiled silently.
  • 703cb259 Pit_SetPropertyStr no longer interns. ~796 call sites each took rt->ct_lock, a process-wide mutex, on the runtime’s hottest path. Every bench got faster. Two measurements corrected the plan: the ct arena drops only 9,928 bytes (the table dedups — largest caller by count is not largest by bytes), and the roadmap’s stated mechanism for the table was already closed. Fixed a latent GC bug in pit_regexp_exec.
  • 4b9482a1 $hook names a pool-backed site at fire time — decided because wake-on-site matches on the name INSIDE the firing turn, so the deferred design cannot express the feature at all.
  • b6a175b2 PLAN_IMAGE_MAX 256 → 1024. The cap had become a wall in ordinary use: test all registers an image per test actor, and the suite could no longer accept a new two-actor test. Independently confirmed by the coverage agent, which measured the wall being hit on its own base.
  • 08a90e88 the stone-6 deletions get observables. Five runtime counters were exposed to Pit and nothing asserted any of them; a cart’s payload form was likewise unasserted, so M1’s ABI-neutrality rule was a convention no gate held. 11 assertions, all falsified. Measured: boot.cart 113 windows all pmac, root.cart 23 all pmcd.

The arc’s position at the end of the run: stones 1-4 done; stone 5 is 5a/5b/5c done and BLOCKED at 5d — the image lane has no in-loop call path, so nothing below the outermost function in a pool-backed actor can be preempted mid-turn (measured: wake letter at 0.051s register vs 0.528s pool, the latter being turn end). That is runaway protection and mid-turn freeze for the whole fleet, not a debugger feature, which is why the lever stays off until it is fixed. Stones 6 and 7 are mapped (plans/stone-6-7-map.md) and share an unbuilt prerequisite neither names: there is no C single-unit pool assembler.

Landing 5c — $hook NAMES AN IMAGE SITE (done); the lever stays OFF, and WHY is named

The delegated call, and the reasoning. 5b stopped on tests/hook_wake.ce matching sites by function name while mach_exec_name_cstr answers NULL for images by design. Two designs were on the table: resolve the name AT FIRE TIME, or record a pool-relative identifier and let the drainer resolve it later. Fire time wins, and not on cost grounds alone:

  • Every firing site sits inside PIT_HOOK_ARMED, so the copy is paid only by a debuggee someone armed, and compiles out entirely without the endowment. That is the line John drew — armed-only cost is fine, per-call cost is not — so the cheaper-at-fire design buys nothing the expensive one does not already give away.
  • Wake-on-site matches on the NAME inside the firing turn (pit_hook_match_check). A name that only existed after a drain could not arm a pinpoint breakpoint at all, which is the durable form $hook.install documents and the only form a person writes by hand. The deferred design does not merely cost more machinery; it cannot express the feature.
  • The ring already has a name slot (PitHookEvent.name). Deferring would mean widening every event with a pool identity plus a lookup surface, to carry what the ring has a field for.

source/mach.c gains mach_exec_name_into, the two-lane replacement for mach_exec_name_cstr at the four PIT_HOOK_ARMED sites. Only the top-level one actually fires for images — the three in-loop call cases all sit under fn->kind == PIT_FUNC_KIND_REGISTER — but all four use it so no reader has to know that. Guarded on Pit_IsText so it never reaches Pit_ToString, which would allocate on the actor heap mid-dispatch; the same rule and the same reason as runtime.c’s dbg_code_name. File and line are NOT recovered: an image’s source positions live in the origin map, not in the pool. No docs page contradicted this — debugging.md’s “no name mapping appears anywhere in the path” is about the origin map not carrying LOCATORS, not about a pool’s own name rows.

The gate: tests/hook_image_name.ce, and it does not wait for the lever. The payload lever decides what the SHOP lowers to; $image is the image lane’s front door either way. So the fixture builds a real one-unit pool, takes its entry through $image, and calls a function inside it while the holder has $hook armed. That makes this live at TODAY’s default and still live after the flip. Falsified deliberately: with the naming removed the site is still RECORDED and only the name is empty — events=256 anonymous=2 named=0, which is exactly the defect’s shape, and the reason “recorded at all” and “named” are asserted separately rather than as one lookup.

THE LEVER STAYS OFF, and the reason is a SECOND defect, not this one. With PIT_POOL_PAYLOAD=1 the baseline is 2262/1 — hook_wake alone — and it is no longer the name. Measured, both lanes, twice each: the holder’s wake letter arrives at 0.051s in the register lane and 0.528s in the pool lane, where 0.528s is also when the worker’s own reply arrives. The pause timer fires exactly once in BOTH lanes (one pausing (turn exceeded 50ms) line in observe.jsonl either way), so pause_flag=1 is set and the pool lane simply does not honour it: the nudge only flushes at ENDTURN, which is no earlier than a drain would have found it, which is the claim hook_wake exists to make.

The mechanism is one line. The only guard between pf == 1 and goto suspend at a backward jump is if (ctx->vm_call_depth == 0). The image lane has no in-loop call path: MACH_CALL, MACH_INVOKE and MACH_GOINVOKE all enter a callee in-loop only under fn->kind == PIT_FUNC_KIND_REGISTER, and an image callee falls through to the “C, native, or bytecode” arm, which does ctx->vm_call_depth++ and Pit_CallInternal — a nested C VM entry per Pit→Pit call. So below the outermost Pit function nothing in a pool-backed actor can be preempted mid-turn. That is bigger than $hook: it is runaway protection and mid-turn freeze for the whole fleet once the lever flips.

It also looks CONTAINED rather than deep, which is why it is worth ruling rather than guessing at: every accessor the register branch uses is already lane-aware (mach_exec_arity/frame_slots/entry_pc/arg_base), mach_exec_resume ALREADY returns into image code in-loop, and mach_exec_from_function already builds the cursor. The loop can already RUN image code with a frame it did not create; creating the frame is the missing piece. That is a change to the hot loop’s calling convention for a whole lane under a copying GC, so it is not taken here — it wants its own landing and John’s call.

THE ONE RED GATE, and it is a WALL rather than a defect: the suite is AT the 256-image cap. ./pit test all is 2690 passed / 1 failed (network/http2, known) with this test absent, and 2683/2 with it present — the second error is start-plan image registry full: 256 images, none evictable, and its victim (build start failed) is arbitrary. Measured both ways on fresh daemons, twice. test all enumerates test FILES, so unwiring from the BASELINE array does not help; only deleting the files does. Two actors is the floor for any $hook test — the endowment refuses target == self — so this is not a property of this test: the suite can no longer accept ANY new two-actor test without raising PLAN_IMAGE_MAX. The registry is process-global and never evicted, and tests/cli/ plan_image_cap.ce asserts the cap deliberately, so raising it is a ruling and not a tweak. The choice is John’s: raise the cap (and update that test), or drop this gate and leave the naming ungated until the lever flips. Everything the enforced CI gate runs — make check-all = lint + check + check-cli + check-web — is GREEN with the test in.

Two dev-loop landmines that cost this session real time, both worth knowing. (1) The FIRST ./pit test run <t> after make all recompiles the fleet and can TIME OUT, and a timed-out test reports as an ordinary failure. Three separate “reproductions” of an inspect_debug interaction were that and nothing else; the whole bisect chased it. Warm the daemon with one throwaway run before believing any single-shot comparison. (2) Other agents’ worktrees share ~/.pit’s build store — one at 99.7% CPU wedged this worktree’s daemon and a tools/pool_fixture.ce at 0% CPU in two worktrees at once. ps aux | grep pit before trusting a hang.

Landing 5 — THE FLEET FROM POOLS: half landed, and the half that is not is NAMED

Status: the mechanism is built end to end and the switch is OFF. Turning it on is PIT_POOL_PAYLOAD=1. The tree is green with it off; with it on, every freshly realized actor fails identically, in one reproducible command, and the failure is in the C START WALK and not in the image lane. This is a deliberate two-landing split (the brief’s own stopping rule) rather than a stall: what is proven is proven, and what is not is a single named defect with a one-line repro.

What moved. pit-shop/mach_lower.cm is new and is the ONE place a unit becomes runnable bytes — the shop’s inline lane and the builder worker each had their own copy of that decision, which meant the payload form could depend on whether the worker fleet happened to be wired. lower_unit asks the lowering for a pit.mach.fragment@1 and links it with shoplib::mach_pool_emit into a one-unit pit.mach.pool@1. One pool per unit is the minimal link plan; one pool per EXECUTABLE — the roadmap’s words — needs the realization’s images/instances rows written and a plan section naming (image, entry), and is landing 5b. The host profile only: a fragment carries this runtime’s rep, byte order and object layout, so a cross-rep or big-endian build keeps the per-unit blob. That is a capability boundary, not a compatibility arm — the forms are told apart by their own magic and the walker runs either.

The C. source/image_provider.c gained pit_private_image_provider_resident_function (adopt + pin + bind + entry in one step, against hashes computed the way the linker computes the ones it stamps), and provider_binding was split so the Pit surface and the resident entrance share one binding builder. source/start_plan.c’s plan_run_unit runs a pmp1 window from where it lies and threads the walker’s result array down as the image function’s owner — so the pin lasts exactly as long as anything the pool produced. cart/cart.cm reads a third window form and cross-checks a pool’s two identity HASHES against the cart header’s strings.

THE DEFECT IT FOUND, fixed and falsified — the closure-frame union arm. A PitFunction’s two arms do not agree on where outer_frame sits: u.pit is {code, outer_frame} and u.image is {binding, owner, outer_frame}. Five reads took the u.pit arm unconditionally — mach_resolve_outer_frame (3) and the fused MACH_BIND_GETUP/MACH_BIND_SETUP dispatches (2) — so an image function’s lexical link read u.image.owner, the realization-state array, and any read of an enclosing frame reported “missing closure frame”. Every GC arm was already kind-aware; the closure walker was the one place that was not. The whole seeded fleet failed to start on it the instant its payload became pools. Why no test saw it: every case in image_endowment/image_import/image_intrinsic is a single expression, so no unit any of them ran ever read a variable belonging to an enclosing frame.

The gate that closes the class: tests/image_lanes.ce. Six closure reads (depth 1 and 2, var and def, read and write, argument-plus-upvalue) plus five real fleet modules run for their exported surface — each on BOTH lanes, each with its expectation named so two identical wrongs cannot pass. Falsified deliberately: reverting the union fix turns depth2 and chain red and leaves the rest green, which is itself the finding — the depth-1 cases lower through a path the wrong arm survives, so a test that only reached depth 1 would still have missed it.

Cache keys — the FIFTH instance of this arc’s recurring trap, and this one predates the landing. The toolchain stamp fingerprints pit-compiler/ and pit-linker/. The pool linker is shoplib/mach_pool_emit.cm and the form decision is pit-shop/mach_lower.cmneither was in the stamp, so editing the pool linker invalidated nothing. Fixed by stamp_file, which folds those two files’ content hashes into the builder identity, plus PIT_POOL_PAYLOAD folded beside PIT_STREAMLINE_DISABLE. Verified the key MOVES: the realize context’s toolchain goes blake2:7232930b…blake2:ba26fd2a… when the lever flips. executable_linker_salt also gained -pools for the realization record.

Also landed, closing a gap shoplib/debug_resolve.cm names in a comment: the origin map is now STORED. shop_store.cache_mach_result writes pit.mach.origin@1 under a key derived from the payload hash, the same derivation as the debug sidecar, so a frame holding only a pool’s content hash finds it with no manifest. Before this, nothing stored the map the linker already produced.

WHAT REMAINS, precisely. With PIT_POOL_PAYLOAD=1, every freshly realized actor dies target disrupted: not a functionbefore its body’s first log.console. It is NOT the image lane: the same modules, linked the same way, run to completion and return key-identical records when entered from Pit through $image (that is half of tests/image_lanes.ce). So the divergence is in the C start walk’s use of the image function — the owner triple, the entry index, the call shape, or a name the walk binds from a pool result. One command reproduces it: PIT_POOL_PAYLOAD=1 ./pit <any .ce> on a daemon started with that env. Landing 5b starts there, and only then moves to one pool per executable. Three places the two entrances genuinely differ, to check in order: (a) the register lane runs a unit with Pit_CallRegisterVM(..., PIT_NULL, 0, NULL, PIT_NULL) and the pool arm builds the entry function and Pit_Calls it — but $image’s own callers do exactly the latter and work, so suspect what the WALKER does with the result before the call itself; (b) the walker reads properties off that result, binds names from it, and finally calls __start_finished on entry 0’s record — instrument which of those three raises; (c) entry 0 is ENTRY_FLAG_BOOT and its window is still pmcd, so the engine is NOT running from a pool, and the from_boot pool arm (which passes a NULL artifact hash, deliberately — the boot medium’s bytes are not a MODULES row) has never executed.

The falsifiable check for 5b is already built and needs no new C: $image.pinned() observed from a fleet actor is 0 before and non-zero after. The provider exposes that count precisely so the eviction rule is observable, and a fleet running from mapped stone is a fleet holding placements pinned. Do not accept a green suite as the evidence — green was already true.

Two things a future attempt should know. (1) pit-shop/shop_build.cm sat at exactly 255 frame slots; adding two top-level bindings made it 257 and the error is honest and immediate. Slots were reclaimed by moving cache_mach_result into shop_store (its right home) and deleting the mcode_mach_result adapter. (2) A reseed with a broken payload form WEDGES THE TREE: the regenerated cart cannot boot, so it cannot reseed itself, and make nuke cannot help because reseeding needs a bootable shop. The escape used was the cold floor (OUT=cold-build sh boot/manifest/bootstrap.sh) plus restoring the committed boot artifacts — safe here only because no ABI moved. Prove the payload form on a script before reseeding with it.

Measured, and worth knowing before 5b: a one-unit pool is 1.5–1.9× its pmac blob. Across fifteen real fleet modules — shop_actor 108,522 → 183,876 bytes, terminal 191,123 → 322,720, shop_build 274,613 → 405,724, mach_pool_emit 80,850 → 105,592 — the ratio narrows as the unit gets bigger, which is what you would expect from per-function descriptors and an un-deduplicated STONE section amortising. That is the case FOR grouping: one pool per executable dedups text across units, and the whole point of the ratio is that it is a link-plan lever rather than a property of the format. Nothing here is a reason to stop — a mapped pool costs no actor-heap materialization at all, which is what the bytes are buying — but a cart’s size is a real budget and 5b should re-measure after grouping.

Landing 4b — THE ARRAY (done, 2026-07-28)

The plan format is now one ordered ENTRIES array plus the NAMES each entry binds. Three kinds and no more:

kindproducesfields
ENTRY_UNIT (0)run bytecodeindex = MODULES row, or ENTRY_FLAG_BOOT = the boot medium’s engine image; a = an AOT dylib preferred over it
ENTRY_NATIVE (1)call a providera = registry symbol (a compiled-in extension, or a raw endowment’s endowment/<name> key); b = dylib path, empty for the compiled-in registry
ENTRY_VALUE (2)read property a of an earlier entry’s result, or (ENTRY_FLAG_BOOT) of the boot context’s globals recordindex = source entry

Entry flags: LAZY (a raw endowment: the spawn context may already have fulfilled it, and then the provider must not run), REQUIRED, BOOT. Name flags: OVERWRITE. Rows: head 40, entry 40, import 16, name 16.

What died. The bindings table and its three fulfilment CLASSES (BIND_ENGINE/BIND_ENDOWMENT/BIND_BOOT_GLOBAL), the five STEP_* kinds, and the separate provides table — provides IS a NAMES row, and an endowment IS an ordinary row. The C walker is now ONE loop (bind imports → produce → bind names) with no binding class and no engine-name list; cart.cm’s decoder and verifier likewise. What survives is a plain floor LIST in cement (ENGINE_NAMES), consulted once at build time to decide which ROW a claim gets — the three-way membership LOGIC that hung off it is gone.

The {value} spelling. docs/architecture/executable-array.md says a {value} is “a constant … or an actor token the runtime fills in at start”. The landing spells the fill-in half as a PROJECTION: property a of an earlier entry’s result. That is what makes $self expressible without the runner knowing the name — entry 0 is the engine unit, and a floor claim is an ENTRY_VALUE over it. engine_claims is now DERIVED (every ENTRY_VALUE whose source is entry 0), not a separate class scan.

The crossing was WRITER-ONLY, and cheaper than the brief expected. roadmap:750 is right that writer and reader cannot move in one pass, but the split does not need a temporary C reader: the entry array went in as a NEW SECTION KIND (11) beside the existing PLANS (kind 10), so phase 1 = cement emits BOTH and the shipped binary ignores kind 11; phase 2 = the readers move to kind 11 and the kind-10 writer is deleted. Every reader already tolerated extra sections (default: break; verify pins only the first 7 in order, then ascending kinds), so phase 1 cost ~40 lines of temporary Pit instead of ~230 lines of temporary C. Kind 10 is retired, not reused — reusing it would have made a phase-1 cart ambiguous. Both phases reseeded and crossed green on their own.

Trims (scope narrowed mid-flight by John — delete only what is genuinely dead, and report the rest): $overling_id and bare self DELETED from ENGINE_NAMES, engine_value(), and (for $overling_id) its now-unused engine_lite local. Both were provably unclaimed — no .cm/.ce in the tree names either identifier. Nothing else was converted; the per-name survey is in the agent report.

Behaviour changes, both deliberate: (1) a name bound from ANY entry now marks the trim’s keep set — under the step/binding layout a provides row did not, so a published name that collided with a boot intrinsic would have been nulled by the trim (latent, never hit); (2) an absent raw-endowment provider is refused at the WALKER, where the claimed name is in hand, instead of inside the row runner — the message text is unchanged.

Line counts: source/start_plan.c 752 → 757; engine/engine_lite.cm 767 → 765; pit-shop/cement.cm 1096 → 1134 (the entry builder is longer than the step/binding pair it replaces because claim rows are now real entries); cart/cart.cm 1300 → 1313. The array did NOT shrink C — it made one loop out of four mechanisms. The shrink is phase E’s.

Gates: make check ALL GREEN · check-cli ALL GREEN ×2 · check-arms 14/14 ALL GREEN · lint OK · manifest byte-stable on the second run · budget OK (record_grow −19%, gc_churn −18%) · check-playdate ALL GREEN · ./pit test all 2689/2690, sole red network/http2 (the documented one). tests/fixtures/plan_image_cap_child.ce’s synthetic image moved to kind 11 — the only test that decodes a section table by hand. One non-recurring red on the first test all: inspect_debug “frames: cannot inspect the current actor”, passing in isolation and absent from the re-run — a load-dependent intermittent in $inspect, no relation to this format.

Landing 4a — the MECHANISM done, one of four families moved (checkpoint, merged)

The resolver arm is real and proven at fleet scale: lang-endowments/ (fallback/parallel/ race/sequence/time_limit) over lang/requestors; endowment_unit_for_claim (locator probe, MEMOISED — log is claimed by 206 files, unmemoised is quadratic; two-packages-one-name errors); the claim rides the closure walk’s same edge list as imports; the binding is a PROVIDES ROW on the producing step, NOT a binding row (the binding pass runs before module steps — a $name-to-unit binding structurally cannot be one). Zero C. engine_lite 1767→1336, ENGINE_NAMES 36→31. One-reseed crossing exercised at scale (provided-names wins; the two-phase safety is the seed generation, not precedence).

4a-CONTINUED recon (attempt 1) — proven, reverted clean, three rulings made:

  • Crossing is TWO-PHASE for a new engine value the fleet claims (forge.ce itself claims log→$letters, so the old cement can’t boot forge.ce to build the new runtime — roadmap:111). Proven-green recipe: Phase 1 adds $letters to the engine, changes nothing about logging; Phase 2 creates the modules + removes log from the engine. Both pure-Pit (new C forces a rebuild that re-triggers the forge.ce boot cycle). SUPERSEDES the brief’s “one-reseed”.
  • ZERO new C: $log_sink is the floor’s EXISTING boot_context.log (pit_engine_log); a module-claimed raw $log_sink is unbuildable (the same forge.ce cycle). The module needs no sink — its no-target branch is $log_enabled-gated off.
  • THE BLOCKER, ruled (A) in edicts: C boot pre-injects context log (pit.c:180) so a set-if-null provides row is a no-op and the module is dead. provides→overwrite restores base semantics (log was overwrite via its binding row). ONLY log is affected; send/actor-intrinsics are not pre-injected, so they’re unblocked/unaffected.
  • 4b-READINESS from recon: survivors do NOT approach 5. NOT-inject-kind names that keep the ENGINE_NAMES table alive until otherwise accounted: $trace (live turn trace-state), $couple/$stop (death floor, §6), $log_enabled (the gate, floor per the log ruling), $letters itself. These are engine BEHAVIOR, not injected values — 4b cannot delete the table while they remain. Flagged.

4a-CONTINUED (resumed) — three families, ~763 lines, 26 names:

  • logging (220) → lang/logging.cm — the deleted extraction is sed -n '310,528p' engine/engine_lite.cm at 43f08622; needs $log_sink (the C bootstrap sink pit_engine_log, genuinely boot-private).
  • messaging (389) + actor intrinsics (154) → against the $letters contract the agent designed: {receive(fn)/deliver(msg)/on_exit(fn)}, engine-provided, claimed — receive appends to a consumed-if-true chain, deliver runs inline (the root shop self-logs via handle_message today), on_exit carries remove_all_portals. Refusal is DELIVERY-side in the floor (push-side would need a C mailbox bit — not added); the floor replies to the sender immediately when a reply id is present. Death floor STAYS (edict §6, ruling (i)).
  • The 255-ceiling bit the CLOSURE-WALK CALLBACK (an anonymous nested frame at exactly 255), not the module body — mcode compiles clean, streamline dump shows 172, the real number is ONLY on the error channel (.pit/log/observe.jsonl filtered “channel”:“error”). Cost the agent three reseeds; the next one must watch that channel.

4a’s two forks ruled (orchestrator, doctrine-determined; FLAGGED FOR JOHN)

Q1 = (d): the resolver arm serves BOTH claim lists; log.cm provides the bare name log. Edict 5 (files carry their own names — the $ sigil is the DENIABILITY marker, not a naming rule) + endowments.md’s own “everything applies to both” s

The array agent proved items 1-3 CANNOT land without item 4 — an entry’s kind is written at build time, so cement deleting ENGINE_NAMES requires the shop RESOLVING a $name claim to a unit, and that lane does not exist (std_endowments is .c-only; endowments.md’s lang-endowments/delay.cm shape is documented, unbuilt; claims aggregate as names only). Option C ruled (orchestrator, sequencing): 4a = the endowment-module lane on its own gates; 4b = the array, then mechanical, docs true in one shot, no inject-row caveat that the full design would delete. CORRECTED NUMBERS: start_plan.c is 743 lines (not 870); “engine_lite drops ~1,870” is impossible (the file is 1,767 total) — the real movable library is 1,196 lines (logging 220, actor intrinsics 154, messaging 389, requestors 433), engine ends ~350-420. The baked-caller map is COMPLETE (4 files: cement’s plan writer, start_plan’s walk, cart.cm’s second full reader feeding cart info/verify AND the ship extension spec, cart_boot’s index) and the 4b crossing is genuinely TWO-PHASE per roadmap:750 (pit_init needs image 0’s PLANS before any pit runs — writer and reader cannot move in one pass). The proposed entries/names spelling (kind-dispatched rows, names as a separate sorted table, {value} splitting literal/inject) is BANKED for 4b — minus inject, which dies under 4a.

Canonicalization DONE + NUL injectivity DONE (merged together, 2026-07-28)

Canonicalization: ten writers + SIX derivation-key sites + the realize context key + the recipe fingerprint through the one encoder (the crossing itself proved keys had to move: identity_seed folded plain encode → the same executable keyed two ways cold vs warm — caught by the cache gate). Nulls were NEVER the divergence (json.encode drops them too); only key order. There were THREE canonicalizers, not two — pgo.cm’s survives with its writers killed; ITS DELETION IS QUEUED for pit-compiler’s next owner (its null arm is the documented gotcha, latently broken). Sort cost honest: 6.92× encode, ~50ms/unit, cache-miss only. BONUS REAL BUG: nota.decode(empty) read UNINITIALIZED MEMORY — refused now, like wota, spec’d. network/http2 verified failing identically at base — real pre-existing defect, not environmental. shop_build.cm is at EXACTLY 255 slots (paid for the encoder binding by collapsing a duplicate) — the ceiling now presses on the biggest shop module; evidence for the eventual rework John predicted.

NUL injectivity: blob.make’s contract is UTF-8 bytes (proven three ways); make/write_text/read_text + kim.encode/decode carry NULs via length-accessors; 11-text corpus proves NUL-free bytes identical — the store doesn’t move. Rep-level C test added (check-arms runs the C suite, not language tests — the PROF3 gap, noted). Latent GC hazard fixed in write_text. BANKED FOR JOHN: character(0) returns EMPTY TEXT — pit source cannot spell U+0000 (runtime.c:5712, the same C-string defect one layer up); fixing it changes what “\0” compiles to → moves constants + realization hashes tree-wide (one crossing). Language-semantics ruling. Also banked: json.decode silently ignores bytes after a NUL (needs a length-taking parser); docs/library/blob.md:95’s write_text description is stale (says kim-length; the C has always been a byte-count prefix).

Cart path-neutrality DONE (2026-07-28, 8510895f + reseed 97cece72)

The committed boot/root.cart baked the seeding checkout’s absolute path — ONE leak: compile_engine_lite (shop_build.cm:2521) passed the absolute runtime_path as the compiler’s debug-filename argument, which stamps onto every function (89 copies in the cart’s mcode window). Fix: pass the stable locator engine/engine_lite.cm; slurp still uses the absolute path. Proven: strings root.cart grep for host paths EMPTY, same cart SHA across three reseeds AND a copy at a different path, cold boot from bootstrap.sh at a foreign path → healthy 10-actor fleet + fresh-realized actor OK. engine_lite.mach never leaked (v2 mach drops per-function filenames — why only the cart’s mcode window differed).

CORRECTION the agent proved against the brief: the baked path was DEBUG METADATA ONLY (traces/crash dumps; never opened) — a foreign-path cart cold-boots FINE (negative control run). So attempt-3’s log is not a function wedge was NOT the path: it is a stale-cart-vs-new-binary semantics skew (the agent hit its current form as intrinsic log must be called as log.channel(...)). The path fix removes the reseed-per-worktree friction (identical cart everywhere) but does NOT absolve cart/binary skew — recovery for that stays make nuke, never git checkout boot/root.cart. docs/spec/pipeline.md corrected (root.cart/engine_lite.mach ARE tracked; the no-host-paths invariant recorded).

During 4a-continued’s test provisioning, a symlink planted inside a test temp dir (shop/runtime → the agent’s worktree root) turned the test’s file.rm(DIR) cleanup into a worktree deletion: rm’s recursion decision uses provider stat, which is stat(2) — there is NO lstat anywhere in the C tree — so a symlink-to-directory reports isDirectory and gets recursed THROUGH. The main checkout was untouched; the agent worktree was restored via reset (all uncommitted landing work lost; the agent held every file in context and reconstructed). Fix dispatched immediately per the no-chips directive: C lstat capability in platform/shared/file_endowment_posix.inc + per-platform providers, portable isLink, rm unlinks links instead of descending, plus a victim-survives regression test. Standing rule until it lands (and good hygiene after): tests must provision by COPY, never symlink, into any directory that gets recursively removed. Related defects from the same landing (fixed in-lane by the 4a agent): the realization key didn’t fold endowment-fulfilling file existence; make nuke left .pit/objects + .pit/state/realize-index.json (the real wedge — “the escape hatch does not escape”); the claim probe and realize reader used different mount lanes (direct_locator_stat_value vs locator_stat — fix is ensure_package_mount on the resolver return path).

THE HYGIENE AUDIT 2026-07-28 — 16 findings, one root cause in three layers

Commissioned by John after the incident (“also do an analysis for other such dangerous things… Pit should be safe”). Read-only sweep of seven lanes. The through-line: no layer of this system could express “do not follow a symlink” — (1) no lstat/O_NOFOLLOW/ reachable AT_SYMLINK_NOFOLLOW anywhere (FIXED, landing 4052aada); (2) file_core.cm:520-528 DISCARDED the accurate dirent type the provider already returned in favour of the following stat (FIXED same landing); (3) pathlib.join normalizes nothing, so every join(root, untrusted_name) in the shop is a traversal (OPEN — the root of findings 1/2/12/13 below).

OPEN — urgent (dispatch as one hardening landing):

  • tmp_write is an arbitrary-file-overwrite verb: shop_store.cm:384-417, dispatched unvalidated at development_shop.ce:909-915. Any actor holding $shop sends name:"../../../../Users/john/.zshrc" → mkdir_p + truncate + append. All in-tree callers pass computed names; the hole is the verb. Fix: reject /, .., absolute in the handler.
  • Bundle ingest writes unverified network bytes to an unvalidated path: shop_source.cm:2385-2387name is --name verbatim OR entry.name from a policy catalog consulted automatically on a resolution miss; the write precedes verification (failure only renames to .qop.refused). name="../../boot" overwrites the committed boot artifact. Fix: bare-identifier validation + stage to a hash-named temp first.
  • The daemon socket has no permissions and no peer check: zero hits for chmod/fchmod/SO_PEERCRED/LOCAL_PEERCRED/getpeereid in ALL of source+platform (sys_ipc_uds.c:38-69). Any local user can connect and start actors as the shop owner. The /tmp fallback is deterministic from the shop path and triggers at 104 bytes — this worktree measures 98. FLAGGED FOR JOHN (changes who may talk to a running daemon).
  • acopy_tree (pitfs.cm:729-787): symlink-following recursive copy, NO cycle detection, O_TRUNC on destination. ZERO callers — fix or delete before someone reaches for it.
  • walk (file_core.cm:664-732) follows links out of the tree: seen_dirs keys on canonical so link -> / is never “seen” and the walk enumerates the filesystem. Backbone of enumerate/glob → pit build C-file discovery, tidy, publish. Read escape + hang. Also: a repeat canonical dir FAILS the whole walk instead of pruning.
  • bare_hash never validates (shop_store.cm:30-33) → objects/blake2/../../ on the read side from a poisoned catalog. Fix: assert [0-9a-f]{64}.
  • os.open_url is shell injection (sys_os_posix.c:202-215): system("open '%s'"), no escaping, in a FLOOR module. No callers in-tree — cheapest possible moment to fix (posix_spawnp with an argv vector; the machinery exists).
  • pit qop packs through symlinks with no cycle detection (qop.ce:35-78) — a source tree containing x -> ~/.ssh packs the keys into a shipped artifact.
  • Test corpus: 17 tests still on fixed /tmp with recursive deletes (no shared temp-root helper exists — that is why the guid fix went file-by-file and left stragglers); cli/aot_app_bundle.ce pairs cp -R with file.rm; cli/workspace_member.ce OVERWRITES the repo’s own package.json and restores only on the happy path; shop_update.ce rewrites the live lock.json with no restore; two tests recursively delete fixed $HOME/Library/... paths; shop_clone/shop_update recursively delete a git checkout of a REMOTE repo (where symlinks are legal).
  • MINOR: logger/sink_desktop.cm:29 fixed /tmp/pit-loader.log (symlink-redirectable); net_posix_provider.ce:7 fixed socket; profile scripts share fixed /tmp logs across worktrees (silently corrupts results); sys_path_win32.c:63 returns the UNCONVERTED path on buffer overflow; CLAUDE.md documents pit shop install/remove/list/why verbs that do not exist.

CLEAN (verified, valuable): there is no archive extractor in the tree at all — qop/ tar/zip are in-memory readers, bundle “mounting” is a virtual mount-table push, no entry name ever becomes a created path, so the whole zip-slip class is clean BY CONSTRUCTION. Spawn is clean everywhere (one system() — os.open_url; everything else builds argv vectors; windows implements the CRT quoting rules correctly). make nuke names literal leaf paths. pit shop link writes a JSON pointer map — no symlink, no delete. There is no store GC and no pit shop remove. safe_package_path genuinely prevents locator escape. file.remove is unlink-or-rmdir (cannot escape). Store temp names use crypto.random. No test creates a symlink; zero tracked symlinks in git; pit has no symlink(2) binding.

Real lstat on posix/darwin/linux/web (darwin’s stat became fstatat; linux’s statx already took flags — zero C change); truthfully aliased where the filesystem has no symlinks (playdate/psp/dc/n64); windows gets isLink from the reparse bit GetFileAttributesEx already returned. rm unlinks a link instead of descending. Second load-bearing defect found in the same run: cake/forge.cm’s header epoch tracked only *.h, so editing any platform/shared/*.inc — which carry whole provider bodies for six targets — changed no object key and silently relinked STALE objects (nm -u build/pit.bare had neither _lstat nor _fstatat after a “successful” make). Now tracks .inc. Verified at merge: both symbols present in the binary.

THE HARDENING LANDED (2026-07-28) — both lanes merged green

Traversal + corpus (761fd81f + reseed 15dbfca3): walk captures the root’s canonical path and PRUNES anything not under it (a link pointing INSIDE the root is followed deliberately — its contents belong to the tree either way, and the canonical repeat check terminates the loops); repeat-canonical prunes instead of failing the walk. acopy_tree DELETED with a tombstone — its last callers went in phase 8c because the object-store bundle format supersedes tree copies, so the shop-composition lever is that successor, not a new consumer. pit qop lstats below the root and SKIPS links with a counted warning (a qop has no entry kind for a link; skipping packs every real byte and says what it omitted) — proven byte-neutral by packing boot/content with both packers. tests/fixtures/tmproot.cm: make(label) mints a guid root and registers it, rm accepts ONLY a minted root or a path beneath one — safe by construction; 20 test files converted. workspace_member now backs its package.json edit to a file so a KILLED run self-heals.

Shop surface (6e8c461f + a5bed992 + reseed a2936382): shoplib/path.cm gains is_bare_name/is_hash_name beside safe_package_path (with the reason the new ones REFUSE where that one rewrites). tmp_write/tmp_read/tmp_path_request take bare file names only — the read verb was the same hole pointed outward. Bundle ingest refuses the name BEFORE the fetch and stages the download to a random temp promoted by rename only after verification. The hash assertion sits at the hash_path funnel, NOT in bare_hash — bare_hash also COMPARES attacker data, which is how a poisoned bundle gets caught. os.open_url is posix_spawnp+argv; no system() call remains anywhere in the C.

Corrections the agents proved against the audit brief (audit credibility): shop_update DOES restore its lock entry (encode omits nulls) — the hole was accumulation from killed runs; workspace_member’s restore runs on every path — the hole was process death; neither shop_clone nor shop_update checks out a REMOTE repo. And two in-tree tests used non-hex catalog keys, so strict hex was not satisfied by the tree until they moved to hash_text().

STILL OPEN — need John’s ruling (three): (1) the daemon socket has no chmod and no peer check (sys_ipc_uds.c:38-69; zero SO_PEERCRED/getpeereid hits in the whole tree) — changes who may talk to a running daemon; (2) walk still FAILS the whole walk on one unreadable subdirectory instead of skipping — several callers branch on that error contract; (3) logger/sink_desktop.cm’s fixed /tmp/pit-loader.log — a rename fixes nothing (a uid-suffixed name is equally plantable); the only real fix is O_NOFOLLOW at bootstrap_append, a FLOOR primitive shared with observe.jsonl on every platform, which would also forbid the legitimate admin symlink-the-log-elsewhere pattern. Also open, out of the audit’s scope: link-to-FILE inside a package still appears in globfs/enumerate results (containment governs DESCENT only), so pit build would still read pkg/x.c -> ~/.ssh/id_rsa; fixing it changes what those verbs RETURN.

THE SYMLINK STORY IS CLOSED (2026-07-28) — every ruling landed

66fbb4e6 (walk/logger) + e876716e (socket) + reseed 54daa214 + 53137c3c (pack). All three of John’s rulings executed; the audit’s open list is empty.

Walk skips, and the containment apparatus DELETED itself. Below the root a failed list() logs and prunes; only an unlistable ROOT fails. list() composes lstat and entries carry isLink; walk skips a link outright — not included, not descended — so a link to a FILE inside a package is invisible to pit build, closing the audit’s last traversal gap. Because a directory loop can only be built from a symlink and results can only leave through one, skipping links SUBSUMES containment: the root-realpath capture, per-directory canonicalize, prefix test and seen-set all deleted. Net −18 production lines (walker alone −28). Skips are reported on the log, NOT in the return value — walk returns a bare array and giving it a record shape would ripple through enumerate→globfs→aglobfs→list_files→package_view (qop could add links_skipped for free only because its reply was already a record). The files == null branches downstream all SURVIVE and were checked individually: an unlistable root is still how “package does not exist” is reported; none branched on the partial-loss contract.

Root-followability proven two ways (the dev-loop regression): a walk rooted AT a link enumerates the pointed-at tree whole (new assertion), and package listings are byte- identical before/after — 1923 paths across runtime/platform/pit-shop/shoplib/std/logger.

Logger: /tmp/pit-loader.log deleted; log_path() returns null and the sink writes to stderr via a new os.eprint (the desktop arm had NO stderr path — os.print is stdout, and stdout is where --machine JSON goes). Adds 13 lines of C in an existing file (flagged per edict 2); manifest stays byte-stable.

Socket: the audit’s location was WRONG — sys_ipc_uds.c has zero callers; the live listener is the endowment lane (daemon_transport_core.cm$bsd_socket/$linux_socket). Hardening only the named file would have been a no-op. The live socket really was srwxr-xr-x; it is srwx------ now (verified on my own daemon at merge). Mode is set on the PATH not the fd — fchmod on a bound AF_UNIX descriptor returns EINVAL on darwin (measured); the window is not exploitable because a bound-unlistened socket refuses connect, and the test asserts that rather than assuming it. Peer uid via getpeereid/ SO_PEERCRED with a fail-closed arm where neither exists; a refusal closes its own descriptor and the listener re-accepts. /tmp fallback → /tmp/pit-<uid>/<hash>.sock, judged by lstat (a planted symlink is the attack; stat would follow and admit it), refused-not-repaired; worst case 41 bytes vs sun_path’s 104. Windows has NO daemon listener at all (transport maps to unavailable) — honest ENOTSUP stubs, no invented security-descriptor code. New wedge state, deliberate: a /tmp/pit-<uid> owned by you at the wrong mode refuses permanently until removed.

pit build qopconv pack was the last follower — fixed in-lane rather than chipped (John’s standing directive): lstat below the root, skip with the same message. Proven by packing a tree whose link points outside it: the skip is reported and only the real file packs.

Three rulings 2026-07-27 late: empty blobs, canonicalization, the cache-record’s fate

(1) Zero-length blobs are legal — John: “if the length is zero, what are you reading anyway?” Fixed at the BLOB layer, not papered in the store: coherent-on-empty operations (hash, zero-byte read, slice, snapshot) become valid; bytes-requested-from-nothing (read_fit of a width) keeps refusing. blake2-of-empty = the standard digest, b2sum-checked. Dispatched. (2) Canonical encoding GOES — one encoder (shoplib/canonical.cm), EVERY stored artifact through it uniformly (John’s requirement), nulls omitted per spec, the second canonicalizer deletes, one cold reseed. Queued behind spine landing 2 (store-write territory). (3) pit.realization rename + keep-or-kill — John: name it anything; “if it’s that old is it still needed?” The landing INVESTIGATES first: it earns its keep iff it is still the stat-fingerprint fast path the content store cannot provide; delete if dead weight, rename (cache-flavored) if load-bearing. Queued behind landing 2 (same files). Also ruled this block: static over-application stays a compile refusal (edicts).

Diagnostics honesty DONE + wake-on-site DONE (merged together)

Diagnostics: the false arity was FIVE emitters (filter/find/arrfor/reduce + the dot-call proxy) — emit_arity_call PROBES a dynamic callee’s arity and branches per width; the checker read wide branches as over-application. callresolve learns the probe contract (matched by fid AND slot — copy propagation rewrites one operand). The honest lane needed NO phase tags: severity alone, once the false positive died (“blanket abort breaks the seed” falsified — the whole fleet builds green under it). CONSEQUENCE: production flows through compile_result_from, NOT compile_object_result (which has no production caller) — so the 255-slot diagnostic NEVER reached a user until this landing. BEHAVIOR CHANGE FLAGGED FOR JOHN: static over-application is now a COMPILE refusal (two deliberate runtime-arity tests rerouted through arrays); reversing is one severity change at callresolve. Banked: streamline’s dead 3607/3622 arity check would reproduce the false positive if _module_summaries ever gains a producer.

Wake-on-site: install takes an optional match set (names/code_ids, ≤16, refuse-not-truncate, call-events only — returns carry no identity); recorder arms scalar off-heap stores; scheduler flushes at BOTH turn exits (ENDTURN_SLOW is load-bearing — hot-loop breakpoints park there; letter beats the worker’s reply, proven). Holder = id, nothing dangles. Costs measured: structurally zero uninstalled, +0.54ns/event armed. REAL BUG FIXED: code_id was a moving-heap per-closure address; now stable code identity. FINDINGS: the optimizer eats sites (matching sees what streamline LEFT — 122 events, the inlined target’s name in none; the origin-map/inlining story from a new angle); $inspect/$hook code_id identities do NOT cross-match on the image lane (image ptr vs FUNCTIONS-row ptr) — banked for the debugger’s next iteration; residual: delivery bounded by the fast timer, blessed by the ruling.

THE PREMISE ARC, landing 1 — DONE: pit.executable@1 is dead

pit.target.realization@1 produced on every build; executable = the manifest hash; every plain field named in artifact-formats’ “What the shop produces today” subsection as awaiting its D-time ruling (identity/modules/module_order inline-manifest no-target_profile etc. — the spec’s images/instances rows stay UNWRITTEN, not renamed, until pools are the unit). All consumers converted, census +3 (development_shop had FIVE guards). Deleted with the record: schema+schema_version, abi tag, payload_mode (→execution.form), manifest_hash (→executable), exe.context — whose death took 22 lines of unreachable start_plan.c. Crossing: cold one-reseed (linker-salt bump + schema-guard belt), and the self-hosting proof: the NEW fleet reseeded itself. NAMING QUESTION FOR JOHN: pit.realization (the host-local realize cache/freshness record) collides with the target realization’s name — the cache record wants renaming; three-record chain documented at its writer meanwhile. Landing 2 named: the inline manifest becomes a separately stored artifact (moves the publish closure walk + cart record lane).

c1 also DONE (same battery): the scratch estimate joined the first slot check; late backstop unreachable; deliberately conservative (1-4 slot margin at the very top beats a second copy of the shape-lowering’s decisions).

pit debug DONE (merged) — the tool exists; the gaps are named, not papered

shoplib/debug_resolve.cm (pure chain, everything an argument) + shop_tools/debug.ce. Image lane resolves FULLY against a real merged pool (provenance unblurred, tested); inlining DERIVED from spans and named in output (inline_chain is never filled by the linker — a known simplification, not a bug); hoisted declarations say “no source position” instead of line 0. Zero C. BANKED from it, in priority order:

  1. mach v2 serializes neither the pc→line table nor the filename — so the realized fleet’s frames resolve nothing (measured: zero line-carrying frames after a reseed). Two fields in mach_serialize_write/_read_v2 + size. MODEL-FIT, dispatch after c1’s mach.c merge (same file).
  2. The image-lane key gap: $inspect frames report code_id as the MAPPING ADDRESS; the origin map keys by pool CONTENT HASH, which MachImageView already holds unsurfaced. Small C; rides with #1 or with spine landing 5.
  3. A single-turn runaway blocks daemon service (12.5s–201s to serve pit debug against a spinning actor — the motivating case is unreachable if the target never yields). Scheduler/daemon service isolation — spine-adjacent, needs design.
  4. text(array, separator) silently drops numeric elements — runtime defect, small.
  5. boot/root.cart embeds the absolute runtime path — not byte-reproducible across worktrees (differs by exactly the path-length delta). Reproducibility thread, with the canonical-encoding item. Also: docs/architecture/debugging.md should mention the tool (small, next docs touch); --sites exposes a pre-existing locator asymmetry (read_source wants /).

Slot limit DONE (b97e0a2c) + cache completeness DONE (cfd87df8)

Slot limit — John’s hypothesis exact, bisected: every live top-level binding = one root-frame slot; dies at the C lowering’s two checks; the C message existed and was precise — what was missing was propagation (a disrupt carries no value by language design) and the module name. Diagnostic landed in Pit post-streamline (zero C), code mach.frame_slots, boundary tests calibration-derived and falsified both ways. Recommendation RECORDED: no rework — census shows nearest approach 173/255 (82 bindings of headroom; size is not the predictor — the two biggest files have root frames of 17 and 5); both rework shapes make every module pay ((a)-as-stated cannot work: a shaped record’s field index is itself an 8-bit operand; (a′) array-spill ~250-400 lines; (b) prefix-word encoding ~300-500 lines in mach.c + moves the ABI + collides with the fragment emitter). TWO MICRO-RULINGS FOR JOHN: (c1) hoist the C’s conservative scratch pre-pass into the first check (~15 lines C, makes the residual exactly-255-plus-scratch case precise); (c2) let pit_mach_compile_mcode_bin return its built message instead of disrupting (~8 lines, changes the failure contract for 12+ callers). Banked for the compiler pass: streamline emits a FALSE error-severity arity diagnostic for inlined filter callbacks (mcode deliberately 2-arg calls them), invisible only because the production lane discards streamline errors — and the discarded-errors lane itself needs a decision. The ledger’s earlier “~4 functions” threshold was true of the tree mid-manifest-landing, not generally.

Cache completeness — the reframe: cement wota-encodes the WHOLE record, so its read set is every field plus key order at every depth; construction history was the divergence. Fix: the miss path replies with the stored record read back (identical by construction) + allowlist→denylist (the allowlist had silently dropped executables — the real gap; both brief-named causes proven non-defects). Gate falsified both ways. New banked defect (agent chip, invisible to John): catalog_put_value computes catalog_path(key) before its null guard — a null key panics the write path instead of returning false; pre-existing, surfaces via ./pit <file>.ce scripts calling prepare_executable.

Docs premise-alignment DONE (merged 831f5e41) + spec questions banked for phase D

New docs/architecture/executable-array.md (the owed D page); eleven pages corrected (endowments.md claimed “exactly two forms” and omitted {native} entirely; debugging.md had invented a “variable map” sidecar and attributed suspend to $hook; pipeline.md still put the linker in C). All-link verification against the built site: 0 bad. Spec questions BANKED for D-time rulings (verify-only findings, not yet actionable): realization@1’s instances rows have a byte shape for only the {unit} kind — {value} and {native} rows are unspelled; pit.provider.registry@1 + its per-endowment adapter executables are exactly the apparatus the array model dissolves, and no plan says their fate; pit.target.start@1 states its own manifests-array divergence; the image-provider spec section never says $image; and mcode has NO slot→source-name table — the debugger actor’s “named locals” ultimately wants compiler emission of one (model-fit, note for the compiler pass). REAL BUG FOUND: cold bootstrap is broken — the rendered bootstrap.sh still builds the pre-forge two-artifact runtime (links the deleted libpit_runtime.dylib with @rpath; the produced pit dies “Library not loaded” before forge can run). The meson landing’s deliberate byte-identity below set -eu faithfully preserved a dead product shape; the fix is in cake/manifest.cm’s renderer (single static binary), dispatched.

The tooling prerequisites arc (John, 2026-07-27 evening) — opened

John’s compiler-pass prerequisites, with status at open: store-freshness (structurally done via the salt fold — now getting a FALSIFIABLE GATE, dispatched); blob→mcode translation (pit.mach.origin@1 DONE today, unconsumed); the debugger actor + endowment (NOT built — phase C2’s census: nine of ten introspection C functions are declarations only, the trace hook has no installer, debug.c is stubs); pause-and-inspect (no suspend/resume exists; $vm’s heap walking does). Dispatched: (A) the store-freshness gate — compiler-edit test, daemon-staleness test, realize-key sweep, deliberately falsified; (B) $inspect’s primitives + scheduler suspend/resume + the $hook installer, debug.c’s importable surface dying, read-only contract held (set_closure_variable stays unimplemented), suspend’s power-home proposed for John (candidates: $runtime or its own surface — $inspect stays read-only). NEXT after B: the debugger ACTOR — GDB-shaped, consuming $inspect + $hook + the origin map + @3 spans, source-line stepping for the compiler pass. Boot story (phase E: fixed-header-only C) remains scheduled after the arc’s final landing.

Stream 2, landing 3 — DONE: pit.executable.manifest@1 + one-selection-per-package

Merged. The shop’s resolution produces the manifest on every build (workspace snapshot + resolution objects got their producers; shoplib/canonical.cm is the ONE canonical-record encoder); the collision refusal is live, names both identities with root-to-unit paths, falsified deliberately. The manifest hash folds into exe.identity.hash — the fold WAS the crossing (stale caches key nowhere; no scaffold). Spec corrected: packageless units (eval/absolute-path scripts) get the reserved <script> package row; native-provider imports produce no binding row; the sort key is structured (package, path), never locator text. Zero C.

Items 3–4 of the brief are their own landings, ruled sequencing (orchestrator): next is (3) the portable-unit lane — the realize step consumes @3 through mcode_lower.lowering_code/unit_blob (the ALREADY-EXISTING reader, currently dead code — NOT compiler.target_lowering_unit, which takes the internal object; the brief was wrong and the agent proved it), retiring shop_actor’s compiler import; then (4) pit.executable@1 dies — the big consumer conversion (cement ~15 field reads + EXECRECS, cart.cm, cart.ce, bundle.ce, development_shop.ce, shop_realize.cm, the start door, six tests), each target payload moving into the realization.

Brief corrections proven: mcode_unit_result DOES have a production caller (mcode_artifacts.cm:247 via precompile←compile_worker←build.ce) — “no production caller” is true only within pit-shop. Latent defects found while mapping: shop_actor.ce:479 reads exe.context which nothing writes (dies with landing 4); cacheable_executable omits endowments_by_module + program_dylib_path that cement reads — cached vs fresh executables cement DIFFERENTLY (chipped). COMPILER-WAITS item #3: adding ~4 live top-level functions to shop_build.cm makes mach lowering disrupt with no diagnostic — bisected to liveness, not size or construct; workaround was structuring the manifest builder as its own module (which was better anyway). test-all baseline motion: base 2522/2523 with network/http2 the sole persistent red; watch_package is a daemon-session singleton intermittent (pre-existing).

STREAM 1 COMPLETE — meson is deleted; forge is the build system (landing 4, merged)

All four landings closed. The forge CLI grew exactly: --set <group>.<lever>=<value> (stamp.* only, revalidated), --define, --flags, --tests, --opt (now reaches the link line; default -O3 — -O2 was a proven 36% budget regression). check_arms.sh derives from the host recipe + override sets, 14/14 arms build AND run the C test lane. Test discovery: *_test.c in source/, tests/, and every dir the build takes platform C from (daemon_lifecycle_test is found on posix hosts STRUCTURALLY); skip-77 self-skips replaced all five per-arm build conditions; PIT_TEST_SOURCE/CFLAGS/LDFLAGS/STANDALONE/RUN macros scanned like PIT_USE_PROBE. Deleted: meson.build, meson.options, the mingw cross file, build_id.h.in, liburing (vacuous), libpit_runtime in every shape, check-builder (folded into check). Per-platform defines moved to compilation blocks. bootstrap.sh’s executable portion proven byte-identical (sha match below set -eu); cold path verified end-to-end (no ./pit → bootstrap.sh builds one → forge builds the real one). Found at base and fixed: nan32_abi_test asked the wrong macro (fixed32 also defines PIT_OBJECT_ABI_NAN32 — meson’s name-gate hid it), two missing qbe stubs, cli/app_bundle red since the fleet deletion (its invocation now passes –with pit-shop/logger), the windows CI job dead on an unknown option (converted to forge; CI is its only verifier). make check-web still needs EMSDK_PYTHON ≥3.10 on this machine — unrelated to meson, build_web.sh was already manifest-driven.

Meson-deletion recon + the arms-are-overrides ruling (orchestrator, 2026-07-27)

The landing-4 agent’s recon stopped on two questions; answered by applying John’s “composition parameters are ARGUMENTS, not a schema” precedent (the builds/*.json unwind): profile arms are forge OVERRIDES over the host recipe (--set stamp.text=kim8 style), NOT committed recipes — an arm is a per-invocation QA variation, not a target anyone ships; the override validates through the loader and lands honestly in the binary’s profile string; check_arms.sh stays host-agnostic (host recipe + override sets), Linux CI keeps the gate, and the 10-duplicate-manifest problem never exists (manifest_systems() untouched). FLAGGED FOR JOHN — same reversal cost as ever if he reads it differently. Also ruled with it: non-stamp arms (validate/force-gc, debug) are forge --opt/--define arguments; test discovery = *_test.c + skip-77 self-skips + the PIT_TEST_* macro family (the PIT_USE_LANE facts-in-the-C pattern); capacity_harness→capacity_test; libpit_runtime.dylib dies (single-static product) with pit.bare emitted for the pack/dist-vm consumers; check-builder folds into check; liburing deleted as proven vacuous; bootstrap.sh gets its one lying comment fixed with the executable portion proven byte-identical. Recon facts worth keeping: only 4 scripts actually invoke meson (the 8 console/web scripts were already meson-free); the windows CI job is broken AT BASE (unknown with_qbe option — meson dies before building).

Stream 2, landing 2 — DONE: the pool linker (take four, merged)

pit.mach.fragment@1 rides the compile result beside the debug sidecar (pmac byte-identical); linker shoplib/mach_pool_emit.cm (~640 Pit) writes all nine pmp1 sections, dedups STONE by bytes, function-relative VALUES (no constant operand ever rewrites), patches only shape operands, origin map from the provenance column. Proven through the C reader on a committed two-unit fixture (self-skips by asking the image — recomputed profile/ABI comparison IS the axis test). Net +315 runtime C (overshot the ruled ~200; reduction work item: ~60 lines of per-function descriptor building could pack into one blob Pit unpacks — plain integers, no authority loss). Named deviations: big-endian emission refused-not-built (no target asks); no cross-unit SHAPES dedup (contiguity makes relocation a single +base; spec doesn’t require it); stable_function_hash = fash.text(stable_id) chosen and written into the spec. stable_id_value forced stone-bytes-on-request (mach.stone_text) because the stable id is linker-composed — no lowering could have emitted it.

COMPILER-WAITS item #2 (with hpack): record literals carry no source positionmcode.cm:3666 pushes ["record", …] directly into s_instructions, bypassing the stamping path, so record:N sites don’t exist and a shaped record’s construction site resolves to no origin. Oracle: every site id dump shows array/push/store present, record absent.

CLI-flake note QUALIFIED: cli/terminal still has a real timeout-shaped flake, proven 1-in-4 at base 2d72eb38 with the landing stashed — aggravated by other worktrees' daemons squatting /tmp/pit_*_e2e and by a timed-out run leaving its own pit shell squatting the next one. descriptor_terminal’s SIGABRT stays dead; the timeout flake is environmental and distinct.

fash64 + mcode-start landed (43c466f3); the emitter awaits ONE ruling

Landed green: shoplib/fash.c (73 lines — pit_key_hash + raw fash64-over-words, as BYTES; two-sided C/Pit conformance table that caught a hand-transposed constant), the mcode-start repair (shop_actor hydrates the v4 sidecar via compiler.target_lowering_unit; witness green), sound malformed-executable construction in the test, origin-map span example fixed. ACCEPTED DEBT: shop_actor.ce imports the compiler until the executable lane carries portable @3 units instead of the internal {code, lowering} object (mcode_unit_result has no production caller) — dies with the executable-manifest landing.

OPEN FOR JOHN — the pool emitter’s shape. Nothing writes pmp1; the C lane writes per-unit pmac. RECOMMENDED: the C lowering emits a RELOCATION TABLE beside each pmac (positions of constant/import operands it just wrote), and mach_pool_emit.cm is a PURE LINKER — lays out sections, dedups stone objects BY BYTES (C already produced target layout, so Pit copies verbatim and never constructs a header — dissolves the second-C-accessor question), patches listed offsets, writes only the documented public format (section directory, SHAPE/SHAPE_KEY rows, displacements). No second codegen, no second object-ABI authority. Matches mach.c’s own comment (“N mcode -> 1 pool is Pit”) and John’s “lowering and linking step”. Fallback if the C emitter must not grow: an operand-class table in Pit (a second place knowing instruction shapes — worse). SEQUENCING: emitter take-3 dispatches on the ruling; MESON DELETION (stream 1 landing 4) follows its merge — they collide in the C-test lane.

test all is honest (chip session, merged a336b1f2) + the batch merge

./pit test all swept drift from five landings that only it could see (:: boundary, symbol flag day, code-cache policy, BUILD1 sidecars, -e removal) — 21 of 24 reds fixed, 2505/2511 exit-1-honestly. One REAL shop defect: shop_actor.ce:516 sent catalog: but the field is executable_catalog — every started actor’s $start resolution catalog was silently null. pit time revived into shop_tools. The six remaining reds are three defects: (1) hpack huffman_encode miscompile — context-sensitive (correct in one realization, wrong in another), oracle huffman_encode("t") == 4f, suspect streamline, fuzzer blind to the class → COMPILER WAITS, recorded for the compiler pass; (2) shop_tools/mach_emit.cm — a Pit-side duplicate mach serializer predating the pmac container; consumers = its CLI wrapper + its test only; JOHN’S CALL: delete (edict-1 default; superseded by the coming Pit pool emitter) or port; (3) mcode-start lane speaks the pre-v4 unit shape in mcode_lower.cm → folded into the pool-emitter dispatch (same file, same surgery); a second bug under it (cache_executable_mach_blobs panic on fresh mcode-mode prepares with script modules) now surfaces its real error. Runtime observations: PLAN_IMAGE_MAX=256 scratch registry never frees (chipped); the morning SIGABRTs were the spawn-from-close bug, since fixed (6ca14dc1).

Merged in the same batch: the text() round-trip fix (c8de594a — %.15g→escalating precision; 1e300-null is CORRECT short-float rep behavior, values.md); forge (88bd170b); literals[] (7c68bd2c). The $image, fash64-minimal-C, and number-REVISIT rulings are in edicts.

The check-cli intermittent is DEAD — descriptor_terminal SIGABRT fixed (merged 2026-07-27)

John’s chip session root-caused it: pit_platform_process_spawn_impl used by-value callback/spec across allocating calls; a major GC in the window left stale from-space pointers — record reads silently rescued by OBJ_FORWARD chasing, the stale callback stored as the VM entry frame’s function, first captured-variable read hit the pit_ptr_in_actor_space assert → abort. Proven live in lldb. Fix: root at impl entry, .val at each use; same for the latent copies in build_argv/build_env. 30/30 clean (was 6-in-12). Consequence: cli/terminal + cli/descriptor_terminal failures are NO LONGER attributable to a known flake — after this commit, a cli failure is real. Environmental caveat that remains: stale daemons from other worktrees can squat /tmp/pit_*_e2e shop paths and rot e2e runs — check ps aux | grep pit before chasing.

Stream 1, landing 2 — DONE (merged c68d5965): cake reads the recipe

The target tables are dead: MANIFEST_SYSTEMS/PLATFORM_MANIFEST_DIRS/LINKED_C_PACKAGES, the 11-branch platform_runtime(), per-target link flags and static-extension rows, payload_policy() and app_fleet() all deleted; cake derives every answer from recipes/*.json + each platform package’s requirements (now carrying its runtime C list, link tokens + link_probe groups, and provides for hand-defined modules like internal/os). Platform names are flat ("darwin") via nested-root resolution — platform/<name>/ is the one nested package root; the directory MOVE was rejected on evidence (meson compiles platform/playdate/sys_thread_single.c into a host test and picks platform/ios/main_ios.c as main_source; moving breaks check-arms, and meson is untouchable until landing 4). The symbol flatten (pit_platform_darwin_*pit_darwin_*) proved contained: all 12 manifests changed in the symbol column only. No crossing fired.

Build descriptions briefly existed as files and were UNWOUND same day by John’s ruling (“these look like command line args… the agility of a makefile” — edicts, “composition parameters are ARGUMENTS, not a schema”). builds/, shoplib/build_description.cm, and --build are deleted; pit bundle takes --payload (default mach, refuses unknown forms) and the existing --with IS the fleet (none = empty). Committed defaults live in the invoking makefiles/scripts (playdate lanes pass --payload mach and no --with). Kept from the landing: --target selects dependency arms only and never implies a fleet; payload_policy()’s dead fields stayed dead. The DEVELOPMENT_FLEET in shop_build.cm is now just an inline default at its one call site — no builds/development.json will exist; if it ever moves, it moves into the invocation.

Agent-proven census corrections: cake.ce’s core_plan message had NO sender (dead, deleted with FLEET_BASE/fallback_fleet()/resolve_ship_plan(s)()); the scripts never read payload/fleet (the whole live surface was bundle.ce→cake); bundle.ce’s offline fleet mirror already disagreed with cake (psp/dc/ps1 got five desktop services on a cake hiccup); MANIFEST_SRC gained recipes/recipe.cm/symbols.cm (an edited recipe would not have re-rendered manifests). Drift gate rebuilt as ask-cake-ask-the-file, 205 checks, falsified three times deliberately.

Queued from the landing: pit-shop/shop_build.cm’s DEVELOPMENT_FLEET + hardcoded payload_mode: "mach" is the obvious builds/development.json (not moved — crossing surface, bias small); declared natives rows (root: internal/sysinfo+internal/os, shoplib: qopfs) bypass linked_packages scope-gating — they are the per-target arms edict 5 wants gone; the hand-spelled pit_runtime_internal_os_use symbols in each platform’s os_*.c die with meson (landing 4).

Stream 1, landing 3 — DONE: the runtime builder exists (pit forge)

cake/forge.cm + cake/forge.ce, in the cake package so the already-listed bin: [cake] reaches it: pit forge <target> [--out|--cc|--jobs|--opt|--force]. It reads the recipe, derives the TU list, drives cake per unit (plan.compile(tc, file) — the documented compile question, one file in, one object out), renders the static table through cake/manifest.cm from shoplib::symbols, and links. make check-builder (in check-all, last) builds with it and runs both suites against the product in .pit-forge. AWAITING RATIFICATION: the name/home/invocation; the single-binary product; the object store.

Four decisions worth the ledger:

  • The product is ONE static executable, not lib+launcher. Module dylibs already link -undefined dynamic_lookup / --allow-shlib-undefined (pit-linker build_link_cmd), so they never referenced libpit_runtime; static + the platform’s dynamic-export flag serves the dynamic lane identically and deletes every per-platform shared-library spelling (-dynamiclib vs -shared, install_name vs soname, @loader_path vs $ORIGIN) that would have been a target-name branch inside the builder.
  • Probes are the toolchain, not a table. No host-capability probing at all: every unit compiles, a unit whose C declares PIT_USE_PROBE may fail, and when it does the module and its link_probe group drop together. A no-probe failure aborts with the compiler’s error. This is “the toolchain is the gate” implemented rather than restated.
  • Objects are content-addressed in <shop>/build/<hash> (the existing bare-hash store), keyed by argv + source bytes + a header epoch (the hash of every .h under the include dirs). Coarser than depfiles, impossible to fool by mtime.
  • The compiler seam is cc, overridable with --cc. No toolchain table: pit-linker/toolchains.cm is a target-name table of exactly the kind edicts forbids, and the builder does not touch it.

Residue — what neither the recipe nor a platform package carried, and now does (three gaps found by building): -x objective-c and the homebrew include path had no home → platform packages gained the ordinary compilation block (CFLAGS/LDFLAGS), which is where a package already says how it compiles; darwin also gained a link_probe.libgit2 group, whose absence scripts/build_native.sh had been papering over with a hand-added -lgit2 (now deleted — the platform declares it). -rdynamic -lm went into linux’s block the same way. Still uncarried, for landing 4: the host arch (the builder asks sysinfo.arch(), since no recipe describes the machine doing the compiling) and the cross-toolchain environment (--cc is the whole seam today).

Docs ratification sweep — DONE (merged 6b0f7f68 + spec fixes)

21 pages aligned; the finds worth remembering: shop.md shipped a COMMITTED MERGE CONFLICT; cartridges.md labeled the ship window pmac (the kill-listed transitional reader) instead of pmp1; c_native “empty means everyone” survived on five pages; c-build.md used “build description” for three different things (now: build manifest / build program / build description); glossary carried three ratified contradictions. Spec pages were verify-only; I then fixed inline: pipeline.md’s stale boot-artifacts section (no separate engine_lite.mach — the engine pool lives inside root.cart at the header’s offset; artifacts untracked per b54b4d90) and its pool-emitter-in-C attribution; mach.md’s garbled relationship line. Queued behind the mcode@3 merge (that agent owns the file): mcode.md:14’s same serializer-in-C attribution.

Confirmed reading (orchestrator): capabilities.native_payload is a RUNTIME lever (may this binary execute native images at all — the authority axis) and stays in the recipe; the cart’s payload FORM is per-build. Distinct questions, both pages now say so.

Cart@2 dev window — RULED by John 2026-07-27, spec fixed same day. “A cart just carries data — it should be able to hold mcode, or mach blobs, or both at once.” Enacted in artifact-formats.md: POOL rows gain a kind (1 = finalized pool, 2 = mcode unit), mcode rows carry zero stamps because there is nothing to mismatch, mixed carts are ordinary, and cold boot follows the engine row’s kind — map-and-compare for a pool, the single-unit bootstrap lowering lane for mcode. A development cart mounts on every generation; the committed dev cart is exactly this.

Decimal literals — RULED by John 2026-07-27, spec fixed same day. The literal lives in the mcode unit, which is JSON — so it is stored as its exact decimal TEXT spelling (the source’s own token) and a consumer obtains the value with the runtime’s number(text): the runtime, which alone knows its numeric representation, is the only place text becomes a number. No nota surface needed, no C accessor, no coefficient decomposition. literals[] is therefore UNBLOCKED for the pool-emitter landing. Number semantics clarified by John (NOT a ruling that text(1e300) must work): what text() returns for a number is not guaranteed. On a rep that cannot represent the value, the literal itself is null and text(null) is null — honest, correct. On a rep that CAN represent it (1e300 is an ordinary double on nan64; 1e16 “should probably work on most platforms”), text() returning null for a representable value is the defect. Chipped with that framing. The same platform-honesty covers cross-target lowering: the artifact carries the text; the target-final finalizer, which knows its profile, is where text becomes the target’s bit pattern — and an unrepresentable literal is the target’s null.

mcode@3 scope decisions (orchestrator, 2026-07-27, from the agent’s reconnaissance)

The dispatched agent stopped correctly with four questions; answered from standing rulings:

  • literals[] defers to the pool-emitter landing (its only consumer — cross-unit constant dedup into STONE). @3 lands as envelope + derivation/catalog + field restructuring with today’s inline access encoding. artifact-formats.md keeps literals as the target; the two spec pages currently disagree (mcode.md:68 inline vs artifact-formats literals-pool) and resolve when the pool emitter lands.
  • data dies in @3 — proven always-empty (s_data never written), its replacement is literals[]; mcode.md’s false “constant pool” row gets dropped.
  • Provenance leaves the artifact: source: {hash, encoding, kind} only, per spec; the result envelope carries {file, package, kind} for diagnostics until the executable manifest exists.
  • spans[] + sites[] fold in NOW; pit.mcode.debug dies and its readers convert — so the origin-map landing changes only pool-side artifacts against a stable unit format.
  • Roadmap correction: mach_pool_emit.cm exists only in 9466791a, NOT on this branch — the phase-C “rejects @2 at the door” blocker note describes an absent file (fix wording at merge). The inlining-off precaution is moot until the pool emitter is actually built.

OPEN QUESTION FOR JOHN, must be ruled before the pool-emitter landing: the spec’s decimal-literal spelling ({coefficient, exponent}) is not producible in Pit — the decomposition exists only in C (source/nota.h:410), shoplib::nota exposes only encode/decode over blobs, and text(1e300) is null. Options: (a) Pit-side decomposition (rep-sensitive across nan32/nan64/tag32 arms); (b) a small C accessor on the nota module (against edict 2); (c) nota-bytes spelling in the artifact (deviates from the ratified page). Until ruled, literals[] cannot land spec-faithful.

Brief 2 — stream 2, landing 1: mcode@3

You are working on the pit language runtime. FIRST: base check + make + read plans/roadmap.md phase C in full — including the mach_pool_emit blocker note (the “operands must be numbers” panic is compiler-emitted and inlined up to 20 deep; make failures findable by compiling with inlining off before touching anything near it) — and plans/edicts.md. DO NOT COMMIT. ./pit down when done.

pit.mcode.unit@3 replaces @2 and its separate cache wrapper. This is the artifact vocabulary’s first piece: derivation keys separated from content hashes, the unit as a store object with its own catalog entries. Scope: the @3 format per docs/spec/artifact-formats.md (verify the spec is implementable as written — where it is silent, propose and report), the producer in pit-compiler/compiler.cm (~893 lines touched), the derivation catalog entries, every consumer converted, no @2 reader kept (edict 1 — reseed regenerates every artifact). NOT in scope: mach_pool_emit, the executable manifest/realization split (the realization vocabulary FREEZES ONLY AFTER stream 1’s recipe schema lands — per-target endowment lists change realization keys). Expect the two-phase crossing: the seeded fleet’s compiler produces @2 and must compile the @3 producer. The crossing recipe is in the roadmap; the scaffold lives for one reseed and is absent from the final tree.

Gates: make check ALL GREEN, check-arms 14/14, check-cli ALL GREEN, lint, manifest idempotent, budget STAYS green. Report: the @3 schema as landed; the crossing story; what the spec got wrong; everything in this brief that was wrong, proven.


Standing agent-brief boilerplate (include in every dispatch)

Verify base sha; make first; read edicts + roadmap; worktree isolation; do not commit or stage; ./pit down when done; re-run every gate yourself; briefs are wrong often — prove claims against the failure, not a green suite; STOP AND ASK on unscoped design; the crossing recipe, make nuke, and the boot/boot.cart wedge are in the roadmap.

Source: plans/archive/next-arc.md