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):
- JSON ratified.
- Platform names FLATTEN IN LANDING 2 (
n64, notplatform/n64). schedstays in the stamp because the profile string carries it; whether it should is an open checkable question — evicting it later is one reseed.- The recipe describes the runtime only.
payloadandapp_fleetwere 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 relocatespayload_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. - No permission lists anywhere — the toolchain is the gate.
constraintsblocks deleted from platform manifests; the loader validates shape, not possibility; recipes are suggestions. Thepaging“fact” went with them (windowing follows the rep, not the platform). Platformrequirements= 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
pitlanguage runtime. FIRST:git log --oneline -1— verify your base againstclaude/rebuild-from-greenandgit reset --hardif stale. Run plainmakeFIRST or./pitresolves to the main checkout’s daemon. Readplans/edicts.mdin full — “The build products and the recipe” is the design you are implementing — andplans/roadmap.md’s resolution-arc section for the crossing recipe, themake nukelesson, and theboot/boot.cartwedge remedy. DO NOT COMMIT../pit downwhen done.The recipe becomes a real, committed file. Today “target” is 39 sites in
cake/plan.cm, aPLATFORM_MANIFEST_DIRStable, and thepit_profile.hchain. 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’spit_profile.harms +PLATFORM_MANIFEST_DIRS+ per-target script knowledge — same content, new home; (3) a loader the shop and cake both read, folded intorecipe_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.mdpoints 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 +
aliasesas 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-unitpmactransitional 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.
- the
- Debug and PGO ride content hashes end to end:
pit.mach.origin@1keyed by pool hash → {mcode hash, function/site ID, source hash, span}; mcode carries spans; PGO observations in mcode identity so profiles survive recompilation; bundledebugslices carry origin maps + source trees. The origin map lands WITHmach_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.
fecf1e48the git requestor test cloned the repository.config.runtime_pathas a clone source handed git a 1.07 GiB pack on everytest all;pack-objectspinned 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.eabbb23acli/terminalwas never a flake — it was a product hang.pit shelldecided 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 leakedpit shellwe had recorded as an “aggravator” was this hang’s symptom.29412843the 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 resolvedccand build-relevant env were folded nowhere). Receipts, never-cached probes, and a compiler-identity fold. The manifest staleness stamp becamepit cake manifest --check— it folds every input by construction because it is the thing that reads them, at 50ms warm.f2b74681one 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.cmis 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 rememberedPIT_MACH_ABIbump to invalidate anything.stamp_dirstopped 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.f91ba2fban 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. Plusmach_exec_entry_line’s image arm passed libc’sindexas a constant-pool index: a SIGBUS whenever the hook was armed, and it compiled silently.703cb259Pit_SetPropertyStrno longer interns. ~796 call sites each tookrt->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 inpit_regexp_exec.4b9482a1$hooknames 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.b6a175b2PLAN_IMAGE_MAX256 → 1024. The cap had become a wall in ordinary use:test allregisters 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.08a90e88the 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 allpmac, root.cart 23 allpmcd.
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.installdocuments 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.cm —
neither 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 function — before 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:
| kind | produces | fields |
|---|---|---|
ENTRY_UNIT (0) | run bytecode | index = MODULES row, or ENTRY_FLAG_BOOT = the boot medium’s engine image; a = an AOT dylib preferred over it |
ENTRY_NATIVE (1) | call a provider | a = 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 record | index = 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.cmat 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.jsonlfiltered “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).
INCIDENT 2026-07-28: file.rm follows symlinks while recursing — a worktree destroyed
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_writeis an arbitrary-file-overwrite verb:shop_store.cm:384-417, dispatched unvalidated atdevelopment_shop.ce:909-915. Any actor holding$shopsendsname:"../../../../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-2387—nameis--nameverbatim ORentry.namefrom 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/tmpfallback 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_dirskeys on canonical solink -> /is never “seen” and the walk enumerates the filesystem. Backbone of enumerate/glob →pit buildC-file discovery, tidy, publish. Read escape + hang. Also: a repeat canonical dir FAILS the whole walk instead of pruning.bare_hashnever validates (shop_store.cm:30-33) →objects/blake2/../../on the read side from a poisoned catalog. Fix: assert[0-9a-f]{64}.os.open_urlis 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 qoppacks through symlinks with no cycle detection (qop.ce:35-78) — a source tree containingx -> ~/.sshpacks the keys into a shipped artifact.- Test corpus: 17 tests still on fixed
/tmpwith 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.cepairscp -Rwithfile.rm;cli/workspace_member.ceOVERWRITES the repo’s own package.json and restores only on the happy path;shop_update.cerewrites the live lock.json with no restore; two tests recursively delete fixed$HOME/Library/...paths;shop_clone/shop_updaterecursively delete a git checkout of a REMOTE repo (where symlinks are legal). - MINOR:
logger/sink_desktop.cm:29fixed/tmp/pit-loader.log(symlink-redirectable);net_posix_provider.ce:7fixed socket; profile scripts share fixed/tmplogs across worktrees (silently corrupts results);sys_path_win32.c:63returns the UNCONVERTED path on buffer overflow; CLAUDE.md documentspit shop install/remove/list/whyverbs 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.
The symlink fix LANDED (4052aada + reseed 69ddc62d)
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:
- 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).
- 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.
- 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.
text(array, separator)silently drops numeric elements — runtime defect, small.- 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);
--sitesexposes 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 position —
mcode.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-linkerbuild_link_cmd), so they never referencedlibpit_runtime; static + the platform’s dynamic-export flag serves the dynamic lane identically and deletes every per-platform shared-library spelling (-dynamiclibvs-shared, install_name vs soname,@loader_pathvs$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_PROBEmay fail, and when it does the module and itslink_probegroup 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.hunder 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.cmis 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 inlineaccessencoding. artifact-formats.md keepsliteralsas the target; the two spec pages currently disagree (mcode.md:68 inline vs artifact-formats literals-pool) and resolve when the pool emitter lands.datadies in @3 — proven always-empty (s_datanever written), its replacement isliterals[]; 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.debugdies and its readers convert — so the origin-map landing changes only pool-side artifacts against a stable unit format.- Roadmap correction:
mach_pool_emit.cmexists 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
pitlanguage runtime. FIRST: base check +make+ readplans/roadmap.mdphase C in full — including themach_pool_emitblocker 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) — andplans/edicts.md. DO NOT COMMIT../pit downwhen done.
pit.mcode.unit@3replaces@2and 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 perdocs/spec/artifact-formats.md(verify the spec is implementable as written — where it is silent, propose and report), the producer inpit-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