Releases
Release notes for aterm, newest first. Each entry links its tag;
the current download is on /aterm, and every release with its assets and
.sha256 sidecars is on github.com/alabsystems/aterm/releases.
v0.91.02026-09-22
Source release. SHA256SUMS is a per-file manifest of the released tree, with a detached Ed25519 signature (SHA256SUMS.sig) by a machine key named in the attached master-signed roster (aterm-machines.toml + .sig). Verification chain and key inventory: SIGNING.md in the publication engine; the roster pin is committed in aterm crates/aterm-update-core/src/pins.rs.
v0.90.02026-09-21
Source release. SHA256SUMS is a per-file manifest of the released tree, with a detached Ed25519 signature (SHA256SUMS.sig) by a machine key named in the attached master-signed roster (aterm-machines.toml + .sig). Verification chain and key inventory: SIGNING.md in the publication engine; the roster pin is committed in aterm crates/aterm-update-core/src/pins.rs.
v0.89.02026-09-20
Source release. SHA256SUMS is a per-file manifest of the released tree, with a detached Ed25519 signature (SHA256SUMS.sig) by a machine key named in the attached master-signed roster (aterm-machines.toml + .sig). Verification chain and key inventory: SIGNING.md in the publication engine; the roster pin is committed in aterm crates/aterm-update-core/src/pins.rs.
v0.88.02026-09-19
Source release. SHA256SUMS is a per-file manifest of the released tree, with a detached Ed25519 signature (SHA256SUMS.sig) by a machine key named in the attached master-signed roster (aterm-machines.toml + .sig). Verification chain and key inventory: SIGNING.md in the publication engine; the roster pin is committed in aterm crates/aterm-update-core/src/pins.rs.
v0.87.02026-09-18
Added
- Session identities: a session can carry its own agent login.
aterm ctl spawn identity=<name>opens a session whose agents keep their login, settings and skills in<state>/identities/<name>/instead of your$HOME—CLAUDE_CONFIG_DIRandCODEX_HOMEpoint there, set after the env strip, so neither login leaks into the other. The directory is created once (0700), primed with the aterm skills and your aterm hook block; the name folds to lowercase.@<sid> spawninherits the aimed session's identity (identity=-opts out), a cold restore re-injects it only while it still exists, and it is Owner-only likeconnected=.sessionsandaterm ctl lsgain a lastidentity=<name|->column,statuscarries the same word afterfabric_link_age_ms=, and a new Owner verbidentitieslists each identity with its live-session count and which agents have files there (present= a non-empty directory, bytes never opened), describes one, andforget <name> confirm=<name>removes the tree — refused while any live session carries it, and always answeringleft=keychain: sign out in the agent first, aterm never reads, names or deletes a keychain item it did not create. The supervise-agent skill's launch recipe now spawns its worker asidentity=worker, because a shared account's usage limit stops the manager and the worker together. The short help budget rises to 10 240 B for the new row.identitiesneeds this build'saterm-ctl: an older client prints only itsOK <n>count line.spawntakesraise=t|fbeside the longtrue|false, as its summary has always said. aterm drive watchhandles a usage limit alone: it escalates, waits for the reset, probes, resumes and restates your rules — and never exits on a limit. Measured 2026-09-15 16:51 → 2026-09-17 08:55: the manager's and the worker's Claude Code drew on one account, its weekly limit hit both at once, the watcher printedEVENT limited … reset=Sep 19 at 11am (America/Los_Angeles), ran out its--max-sand exited, and nobody could act for two days while a finished A/B sat unread (the worker answered a one-line probe in 23 s once the owner was back). Now, onEVENT limited, the watcher sets the worker'sattentionmeta to the notice (the badge aterm's menu bar shows), posts the same text askind=controlmail to the manager's inbox (--inbox, else its own session) and journalsESCALATED …, once an episode; a--max-sthat would run out before the reset the notice names —resets Sep 19 at 11am (America/Los_Angeles),resets 7:30pm,resets in 3h, the zone read — is stretched to 10 min past it (EXTEND until=<UTC> reset=<text>, once a reset). With the newwatch --resume [RULES]it probes the worker at the reset, or as soon as the screen leaves the notice (a/login, a/modelline), with ONE turn at an idle, empty composer; the answer printsEVENT resumed seq=<n> <its line>(a worker still busy on it after 120 s is waited for, not called still-limited and not probed again), clears the attention and restates the RULES file as one turn (EVENT rebriefed); no reaction within 120 s, or the notice again, printsEVENT still-limited …and the next probe waits 10 min, then 30 — a span (in 3h) is read once an episode, a notice printed again brings no probe forward of the backoff, and a dated reset is placed in whichever year puts it nearest (Dec 31read on Jan 2 is two days gone, not next December's). The journal records every step (escalated,extend,probe,cleared);superviseis unchanged, and without--resumeeverywatchline is as before but for theEXTEND. The supervise-agent skill now says to run the watcher as a plain process under a Monitor, keep the standing rules in a file for--resume, and give the worker its own login (/loginin its window — the owner's call) so one account's limit never stops both.- A provenance-tracked aterm repairs itself, without anyone quitting anything. macOS fixes whether a process is tracked at
exec, from the image and the parent, and the verdict never changes — so an aterm that launched from a tagged bundle keeps tagging every file its shells write, including every object file a release cut produces, long after the bundle on disk has been repaired. Re-execing cannot clear it (the tag survivesexec); only a process launchd mints from a clean image is untracked, which is exactly what the seamless update handoff already builds. It now has the trigger it never had: when the app measures itself tracked while the.appit runs from is clean, it asks to be replaced by itself over that handoff, adopting the user's windows and PTYs. Nothing is quit and nothing is reopened; new sessions are untracked from then on. Shells already running keep their own status until they exit — that part is the OS, not a policy.
It refuses by default and in every direction. The measurement is raw, because three of the obvious predicates answer "clean" for "could not look": process_is_tracked fails closed to true, carries is false for a path it cannot read, and the tag scans return a zero-total scan for a directory they cannot read — so the repair reads measure_tracked directly, reads attribute lists rather than carries, and requires a scan to prove it inspected something. A repair must take the LaunchServices lane or not happen: a forked successor would inherit the tag, so the cold lane, both fork fallbacks in the lane decision and all five runtime fallbacks inside the out-of-band worker now abort a repair instead of quietly forking it. It rides the automatic lane, so the typing-gap refusal, the quiet epoch, the close barrier, the freeze budget, the adoption proof, the Commit CAS and the rollback all still govern it, and a staged update always wins the tick. The authority is a zero-field enum that cannot name another build, is earned from a measured verdict consumed on use, and is bounded to one attempt per process and one repair per chain.
- A second host joins the fabric over the sealed transport (round 16, the m7 case;
docs/FABRIC-SECOND-HOST.md). Behind the off-by-defaultsealedcargo feature (-p aterm --features sealed; a default build — every shipped binary — refuses each surface by naming it, and compiles no cipher: 45 third-party packages foratermand 14 foraterm-link, before and after).aterm link broker --tcp <host:port> --key-file <k> --secret-file <s> <log>serves the sealed wire and is ALWAYS guarded (the pre-shared key is one secret every host holds; the capability is what says which node may publish what), refuses a non-loopback bind without--allow-remoteand a key or secret file other users can read.aterm fabric on --tcp <bind> --key-file <k>runs the fleet's broker that way as well as on the Unix socket (--unix), under the same root-derived label and idempotence rules; this host's own bridges stay on the socket, and awirestep proves the port.aterm fabric mint-for <node-id>|new --out <cap>mints the joining node's eight grants under this host's mint secret (which never leaves it).aterm fabric join --broker <host:port> --tcp --key-file <k> --cap-file <c>checks both files, probes the broker before writing anything, records the node id, installs the files, writes[fabric] command, arms the running aterm and proves a round trip through the remote broker.aterm fabricgains NODES — every node with itshost=,state=,fabric=and live sessions,thismarking this one — and reads a guarded broker through the cap's own faces. Measured on loopback (two scratch hosts, a headless aterm each): a task from host 1's session handled on host 2 and acked back in 298 ms median over 10 — the same as between two sessions of one node (299 ms); each bridge's broker link acks in 1–5 ms.
- The rainbow kitty's cursor throws a spark backward as you type. One grain per typed key, born in the sky band of the caret's own cell and thrown back along the band's row on the hand's own wind — about six cells at 12 keys a second on a retina cell — dying on the wing; one in four, under a hand that has earned its momentum, is a brighter star that arrives, pins and twinkles. It never draws under a cold hand, at a stroll, under reduced motion or with a chime, and a light theme thins it like every thrown grain. It is never drawn on your text: a grain is born only over a band pixel the glyph probe proved clear, flies only over cells proved clear for the sky on every row it crosses, and needs at least one such cell behind the caret or it is not thrown at all — so on an
underlineline whose last glyph sits behind the caret it does not appear and costs nothing. Along a TUI's rule row (Claude Code's input box) it flies in the sky zone under the rule's stroke, its rise held to the headroom under the stroke; over a row of text it is not thrown at all. Its cost is frames, not microseconds: faster than about 6 keys a second the effect engine asks for its frame cadence continuously, about twice the wakes it asked for before (measured on the engine's own 120 Hz cadence over a blank row: ≈60 → ≈125 a second at 12 keys a second; the shipped lane runs the effect at half that cadence, and what the live app pays per wake is not yet measured); the sky's population is untouched, because the spark yields rather than retire a star. Design doc §33.
Changed
- The Windows
agents\<program>.cmdtwin carries the landing wait, and its batch ends on the line that runs the program (2026-09-17; residual R4 of 2026-09-16 closed). While<prefix>\landing\<program>stands the twin hands over to the co-locatedatpkg __landing(which prints the waiting line, bounded byATPKG_LANDING_WAIT_SECS, then runs the currentbin\<program>.cmdas a child and exits with its real code; Ctrl-C during the wait runs the current build; with that atpkg gone the twin runs the store build itself, never an olderatpkgon PATH). The hand-over and the forward are each… %* & @exit /b— one parsed line, no exit line of its own — becausecmd.exeresumes a batch file at a remembered byte offset after every line and atpkg re-lays the twin (and everybin\shim, now written with the same tail) while it may be executing; a bareexit /breturns with the ERRORLEVEL the program left (%errorlevel%on that line would expand before the program ran). The prelude isgoto-shaped (no parenthesised block, so a)inProgram Files (x86)cannot close one early), runs the exe directly (nocall, so a%in your arguments is not expanded twice), and embeds every path without the\\?\verbatim prefixcanonicalizeanswers on Windows. A configured prefix with a trailing\no longer renders"…\pkg\" -- %*(an escaped quote to the exe's argv parser, fusing the prefix with the user's arguments so the tool never ran): trailing separators are trimmed from every embedded path, and a path that cannot be embedded renders no prelude. Shims from before this build still resolve (which,doctor, the sweeps, gc) until their next re-lay. CAVEATS, all unverified — no Windows machine has run any of it; the rendered text and the parser are pinned on the Unix suite: (1) a twin from before this build that is EXECUTING at the moment the next pass re-lays it can, when its agent exits, resume inside the new prelude and run the agent a second time with the same arguments — once per pre-change twin, only during that one migration; every later re-lay is safe by the same-line exit; (2) on the landing path there are two batch levels (the twin, then thebin\shim the verb spawns), so a Ctrl-C typed inside the agent can raisecmd.exe'sTerminate batch job (Y/N)?up to twice when the agent exits, not once.aterm help pkganddocs/DESIGN-which-copy-runs-2026-08-27.md§5 carry both. - The
agents/twin is no longer byte-identical to itsbin/shim — it carries the landing prelude (one[ -f <marker> ]stat ahead of its exports; every reader keyed on the shim's exec target is unchanged). Existing twins are re-laid once by the next pass's reconcile. - The kind-gain ladder's
MeteorArmrow — unreachable for the music box (pushforks topush_v2before the ladder; v2 sets its meteor arm's level inrainbow_kitty_v2.rs), kept so the ladder stays total — keeps the old 0.5 under its own name,METEOR_ARM_KIND_GAIN; only the Shift row moved (SHIFT_KIND_GAIN0.5 → 0.9). The deadShiftarm ofpush's MIN_GAP bypass list is gone (the Shift admission arm runs first).
- A test that hangs the gate is now named, not just its argv. When the per-child wall-clock ceiling kills a
targo testchild, theaterm-verify: TIMEOUTblock reads the child's own log back and, under itschild:line, names the last test binary cargo started and every test libtest reportedhas been running for over 60 secondsthat never got a verdict line — with the--exactcommand to re-run it alone — or says why it can name none (the binary finished and the wedge is in cargo; no test was reported slow). The parse is anchored to the last cargoRunning/Doc-testsheader and reads a verdict anywhere in a line, because aterm-gui tests that re-exec their own binary with inherited stdio tear the parent's lines — 37 such child runs from 21 distinct tests in that log. A slow name is cleared only by a verdict at or after its notice, matched modulo libtest's- should panicsuffix, and never by one printed inside a nestedrunning 1 testsection that was running the same name: a test re-execing itself cannot clear itself, so the name stays named and the note says a child reported it. Measured: on 2026-09-16 the gate's aterm-gui test binary sat 3 hours oncontrol::tests::cross_session_paste_reports_a_dead_spill_peer_as_write_failedand the diagnostic named only the argv, an invocation spanning 12,976 tests over 218 binaries; run against that log's whole 6,023-line section the rule names exactly that test, with 4,873 of the hung binary's 4,874 tests carrying a verdict and the two other tests reported slow in that section cleared by the verdicts that did arrive.
- The rainbow kitty's sky streams three times as far behind a fast hand — 1.05 cells of drift-velocity per key interval instead of 0.35, about three cells at 12 keys a second on a retina cell and a quarter of the spark's, so the two layers read as depth rather than one sheet sliding. Still priced by the observed key interval and never the momentum meter, still standing at a stroll and at rest one star-life after the last key, and still clamped at the first column the glyph probe has not proved empty — now read off the star's own row under both ribbon spellings, where the clamp used to look one row above whatever the spelling and would have walked the sky across an
underlineline.
- The rainbow kitty's sky now streams three times as far behind a fast hand. The stars above the line have always drifted backward at the hand's own cadence — the faster you type, the faster the sky runs past — but at 0.35 of a cell per key interval a whole star-life of drift came to about two thirds of one cell, which is invisible beside the spark the cursor now throws off. At 1.05 the far layer moves about three cells at 12 cps on a retina cell, a quarter of the near layer's, and the parallax between the two is what makes the two read as depth instead of as one sheet sliding. Nothing else about the drift changed: it is still the observed key interval and never the momentum meter that prices it, it still stands perfectly still at a stroll and comes to rest one star-life after the last key, and it is still clamped at the first column the glyph probe has not proved empty — so no star crosses your text at the new share that did not cross it at the old one. That last clause is the one that had to be earned rather than assumed: the clamp used to look one row above the star whatever spelling the ribbon was in, which is the right row under the tall body and the wrong one under
underline, where the band sits in the text's own row. At the old 0.35 share the drift was too short to reach past the cell it was born in and nothing showed; tripling it would have walked the sky across the line you were typing. The clamp now reads the row off the star's own pixels, so the guarantee holds under both spellings.
- The regex engine compiles at opt-level 2 in the dev profile.
cargo test -p aterm-regex's default-budget row scans 3 MB until the 2^26-step budget trips and asserts each trip inside 5 s, a bound that holds where the Pike VM runs optimized; unoptimized on a 2017 4-core Intel MacBook Pro the scan took 2.8–8 s and the merge gate's one red test was a coin flip. With the per-package override the census crate already carries, the row runs in 0.85 s and the crate's lib tests in 1.7 s instead of 16–23 s (measured ABBA, quiet machine, 2026-09-16).debug-assertionsstays on, and every crate downstream ofaterm-regexrecompiles once per target directory when this lands.
- Every file an extraction laid down paid a path-resolved
chmod, plus amkdirand astatfor a parent directory that already existed.write_cappedset the sanitized mode withset_mode(dest, mode)—chmod(2), a fresh resolution of a<prefix>/store/trust/<build>.incoming-<pid>/lib/rustlib/...path — while it was still holding the open handle itfstats one line later for those very bits; and the extractor calledcreate_dir_all(parent)once per FILE, which on an existing parent is amkdirthat failsEEXISTfollowed by anis_dirstat. The shippedtrustsysroot lays 4114 regular files across 918 directories, so that was ~16k syscalls of pure overhead on the one in-process byte loop of an install, all of it under the store lock. The mode now goes through the handle (fchmod, no path walk) and the parent directory is remembered across a run of entries that share it — tar and zip members arrive grouped because a directory walk wrote them, and an archive that interleaves directories pays exactly what it paid before. Same bits, sametree_root, sameop/path on failure.
Fixed
- The worker's end-of-turn report is what it displayed, never its hidden reasoning.
aterm link hook install claude --report-to @<manager>posted the transcript's lasttextblock, but this Claude Code build stores the⏺narration it shows between tool calls asthinkingblocks, and its transcript lags the turn (lines are flushed on a 100 ms timer;Stophooks run at once) — so a live worker's report was a stale one-liner from before the turn's end while its real summary never went. The hook now reports the turn's final displayed message: the last message after the turn's prompt that showed anything, its text and its narration in order. Narration is told from hidden reasoning by the kind Claude Code stamps in the block's signature (measured on structure only across 5,920 blocks); athinkingblock without that mark is never posted, and a turn whose last message is only that posts nothing and says so. The read waits (at most 2 s) for the transcript to catch up with the vendor's ownlast_assistant_message, and posts that text when it does not. - The About byline's 2× ladder test read one host's font metrics as a law. The arm added on 2026-09-17 asserts that
Andrew Yatesoverflows the 240pt hero's 164.0pt column at 2× text scale, which it does by 0.6pt in that host's UI face — a font measurement, not the arithmetic the column is. On a 2017 MacBook Pro (macOS 13.7.8) the same name measures 156.884pt and fits, so the assertion made the ladder's law unprovable there rather than wrong, and the test was red on a host whose rendering was right. The arm now reads the measurement and asserts the law either way — the painted rung is the longest that fits, whole name or short form, never an ellipsis — and a host that cannot exercise the step-down says so on stderr with both widths instead of passing quietly. The column itself, the 150pt no-rung-fits arm and the provenance block it leans on are asserted exactly as before.
- Two merge-gate fixtures read the shell instead of pinning it, so the gate failed inside its own run.
lane_jobsmade an exportedCARGO_BUILD_JOBSa ceiling on the side lanes' job counts, which is what a 4-core machine wants; twogate_contractfixtures pin the driver lane's uncapped cap in their stand-in driver (test "$CARGO_BUILD_JOBS" = 8 || exit 71) and took their environment fromEnvSnapshot::capture(). The merge gate exports that variable itself, so inside a real run both stubs exited before their build arm and the two tests failed for the environment rather than the stage (measured 2026-09-16 on a 4-core Intel Mac, where the gate ran withCARGO_BUILD_JOBS=4). The fixture now pins the variable unset, the way it already pinsCARGO_TARGET_DIRand for the same reason, and a new test drives the ceiling end to end instead: a caller's 2 caps the driver lane to 2, a 16 leaves the lane's 8 alone, and an empty or absent value leaves 8 — measured in the child's own environment, which no test did before.
- A reply after a wake is reported, and a report goes out once. The report hook's reader treated the
Stop hook feedback:line a wake adds as part of the old turn, so when the agent's reply after the wake was not on disk yet and used the same words as its message before, the old line passed for it and the reply was dropped as "already reported". A wake now opens a new segment of the turn; the reader waits for the reply's own line. The vendor's text is matched the way Claude Code joins it (a newline between text blocks, not.), and a report posted from the vendor's text because the transcript lagged is no longer posted a second time when the line lands. - Round 16's cross-host surfaces, after an adversarial review. A receipt counts only from the node the task went to: a third node's
ack re=<off>used to settle the asker'spost --wait-ackand its deadline; it now arrives askind=note demoted=ackand settles nothing.aterm fabric joinrefuses a node that is live on the bus from a root that never recorded it (the same cap joined twice made two bridges share one node's mail and wills).aterm fabric mint-forrefuses on a host that joined another's fleet (it minted under a stale local secret, for a broker that refuses such caps), is refused by a default build as the help always said, never writes through a symlink — its temporary file is now created exclusively under a random name, where a symlink planted at the old predictable name took the cap — and makes an identical cap it finds 0600 before saying(0600).jointells a broker whose handshake slots are full (astream admits 64 at once; anyone who can reach the port can hold them) from a wrong key, and the host that serves the broker no longer shares those slots: its own bridges andaterm fabricuse the broker's Unix socket (aterm link broker --tcp … --unix <socket>), where a peer without the key had kept it out of its own broker.docs/FABRIC-SECOND-HOST.mdnow says thathost=is the node's own word and that a peer without the key can keep new connections out. - A manager whose transcript shows a worker's approval box no longer reads
prompt. The phase reader (aterm drive phase, the fabric'sphase=) took anyEsc to cancelrow on the screen for a box; one under a tool's or a Monitor's⎿output, or with the transcript's later words or a done row between it and the composer, is now history. A manager's footer (bypass permissions on · 1 monitorover the artifact bar) is pinned by fixtures to read busy or idle. aterm pkg update trustno longer says "up to date" about a compiler this machine does not run. The verdict was about the managed store and the question is about the machine: on m3,atpkg: rustc up to datestood for two weeks while everytargo, everycargo +trustand every repo pinningchannel = "trust"ran a hand-placed~/.rustup/toolchains/trust859 commits behind the pin.doctorhad reported that seam twice; the verb the owner actually typed had never been given the question to ask. Two causes, both closed. The single-program lanes (aterm pkg install <p>,aterm pkg update <p>) never re-asserted the rustup seam or reconciled the exec roots — every whole-set pass has done both since those calls were written — so the one verb you reach for when you suspect a stale compiler was also the one verb that never looked at, or healed, the entry answering for it. And the up-to-date verdict itself is now qualified by what the machine RUNS: an~/.rustup/toolchains/trustthat is not atpkg's is named, in the printed line and in the recorded status row Settings ▸ Packages shows, with the remedy left where its one spelling lives (aterm pkg doctor). An entry that could not be inspected reads UNKNOWN, never "fine".- A readiness marker is now a claim about the build's contents, and an interrupted removal can no longer leave one lying. Found on m3:
store/trust/8595/with nobin/at all, 417 MB of orphanedlib/, and8595.readybeside it still sayingok.aterm pkg listcounted it as installed,gcwould not sweep it (it sweeps only marker-less trees), andaterm pkg rollback trustselects the highest retained build below current — that one — whose restore loop would have found no tool at any path and REMOVED every shim on the machine, reporting a successful rollback. The corpse was made by removal order:discard_buildandaterm pkg uninstalltook the tree first and the marker last, so anything ending the process inside a multi-gigabyteremove_dir_allleft a gutted tree still vouched for. The marker now comes down FIRST, durably, in both — from that instant the tree reads as incomplete, which is the truth for the rest of the removal and for any interrupt inside it, andgc's partial arm reclaims it. Markers written from now on also record thebin/they were written over, so the disk can refute them however a tree loses its contents; a marker whose build directory is not there at all is refused outright; androllbackrefuses a target that holds no tool rather than disarming the machine. - The index's stale-pin gate no longer certifies a pin it has just proved it cannot judge. The gate refuses an ALab pin below its repo's newest signed
vX.Y.Zrelease; when the pin was ABOVE that release it printedNOTE — … a pack cut past the last promoteand moved on — a bound returned as a fact, since that comparison establishes only that the release cannot answer the staleness question. The whole trust tuple takes that arm by construction (its bundle is packed from a stage2, never from a tag), so nothing had ever judged it: measured 2026-09-17, trust pinned 8595, its newest releasev0.8.0counts 7888, and~/trustHEAD counts 9454 — 859 commits of compiler, published as index after index of "up to date (index build N)". The gate now asks the repo's own source (the higher ofHEADandorigin/main, counted the waytools/atpkg-pack.shcounts) and refuses a pin below it under the sameALLOW_STALE_PINS=1escape, which prints every frozen row. - Three shell suites over
tools/atpkg-index.shnow run under the gate, two of them for the first time.test-atpkg-index-target-pins.sh(28 checks) andtest-atpkg-index-staging-collision.sh(42 checks) had been sitting intools/wired into nothing, over the script that signs the toolchain index;test-atpkg-stale-pin.shis new and drives a real git checkout against the arm above. The gate fixtures that stub these suites are now derived from the roster instead of re-typing it, so adding one can never again make every fixture report it as missing. - A rainbow-kitty star born just under a TUI's rule no longer lifts onto the rule's stroke. Above Claude Code's input box the sky's stars are born in the zone under the light rule's stroke, and a strike star's slow lift (3 px over its life on a retina cell) carried one born in the zone's lowest pixels up onto the stroke, where it sat for the rest of its life — 4 of every 61 stars on the owner's cell, measured. The lift is now held to the headroom under the stroke, the same clamp the spark's rise takes; over a blank row nothing changes. The stroke band is also one set of whole pixels now, shared by the glyph probe, the birth zone and the clamp: on 17 cell heights — the 1× 7×14 cell among them — the probe's own rounding put the zone's top pixel inside the stroke, so a star born there was born on ink by the probe's own answer, and on cell heights that are multiples of 5 the pixel at exactly six tenths of the cell was called ink. Design doc §5.4, §33.
- The rustup seam follows a dev-linked trust.
aterm pkg link trust <checkout>put the checkout's tools on PATH and every other surface read the link as the decision that outranks the store, but the seam kept building<prefix>/rustup/trustfromstore/trust/current— socargo +trust,rustup run trustand every repo pinningchannel = "trust"ran the installed compiler whiletargoon PATH ran the checkout's: two compilers under one name, doctor calling the seam healthy, andlink/unlinknever re-asserting it. While trust is dev-linked to a sysroot the view now presents the checkout:bin/is one exec stub per tool (the shim body the dev-link itself puts on PATH — targo and tippy refuse to run through a symlink, and a stub names the path, so a tree rebuilt in place is what rustup runs a moment later), each stock name a stub to its Trust tool, and each mirrored directory a link to the checkout's. A checkout that is not a sysroot is refused and recorded with the view left as it stands (and the refusal goes with the seam when the link does), a dev-link with no store build still gets the seam,linkandunlinkre-assert it on the spot,unlinkwith nothing to restore detaches it rather than leave it dangling, and doctor compares the view against the checkout's tools while the link stands — for a store-less dev-link too. - A pass reports the machine's state itself. Every
machine-settings:change a pass printed, and every Settings verb that ran a pass, made the window spawn bareatpkg machine— a second walk of$HOMEunder the doctor's budget plus a dry-run apply over every exposed dir — to confirm a state the pass had measured moments earlier. The pass now prints themachine-state:record behind its change line, from the scan it already walked (a pass that moved nothing and failed nothing knows the posture; one that moved or failed something walks again; a pass whose Spotlight half is off walks once only when its Universal Control half changed something). The window's "This Mac" card opens an expectation on the change line and takes the record as the newest state — a read running meanwhile is superseded, so a walk that began before an apply can never overwrite what the apply reported, and a read that began before the change cannot answer for it — and spawns the read only when no record followed. Settings ▸ "Apply now" is confirmed by the record its short child prints last; the Settings verbs that collect a pass's stdout at exit (Check, every Install —install <name>never re-read before) still confirm with a read at completion, since the record they hand over was measured at the pass's top. - A failed pass's row is never rounded up to a full meter. atpkg calls
end_passbefore it printsseed-partial:/seed-failed:, and the tailer's last read follows the child's exit, so within one poll the ended snapshot and the marker land in either order; the terminal branch of the snapshot reducer completed ANY terminal bar's meter, so the marker-first order painted "partly installed" beside 100% about half the time on the likeliest first-launch failure (a full disk). The completion is for a Success row only, a pass that ended with failures keeps its own live meter rather than claiming a full one, and a failure row carries no meter at all — the bar cannot tell whose pass a standing meter is (one sharedprogress.json; a sibling window's holder, the seed child's row under the update child), so nothing a marker replaces lends it one — and both orders render the same row. mdutil -sreading "Index is read-only." no longer blames a busy machine. The volume-state probe behindaterm pkg noindex verify's refinement knew three answers; the fourth — mds's low-disk-space hold, an index that stands and answers searches but takes no new entries, measured on the owner's data volume at 96% full — read as unknown, so a verify whose control file never arrived said "re-run on a less busy machine" over a volume that could not have indexed it. It is its own answer now, with its own sentence (free space, then re-run; migrating meanwhile is safe) rather than the switch's "nothing needs migrating there", and the probe reads the status line alone, never the echoed volume name.
- The managed
claude/codexrun in the SAME tab, live — and no surface sends anyone to a new tab any more. Owner, 2026-09-16: *"aterm atpkg DID install the latest but it didn't make them available for me. instead, it is telling me to open a new tab. NO! all the latest and best MUST WORK IN THE SAME TAB with live update!"* Measured on the owner's Mac: the tab's zsh (pid 1784, 10:44:24) was spawned by the previous build and adopted across the seamless update (app 10:44:32);<prefix>/agents/and the shell.d hook were created at 10:46, and nothing in an already-running shell ever learned about them — the window front-insertedagents/only when the directory already existed at spawn (so the FIRST tab of every fresh machine missed it too), and the shell integration re-asserted PATH only at load and once at the first prompt.clauderan~/.local/bin/claude,codexran a Homebrew cask that hung, the row read "whatclaudeandcodexrun in new tabs", andaterm pkg doctoradvised "remove or reorder that copy" — against the 2026-09-10 decision that aterm's copy is what runs. What landed (and what the same-day audit of it corrected):- Shell integration (zsh, bash, fish; pwsh best effort): the managed dirs are re-asserted at EVERY prompt and EVERY command (preexec, so a command typed at an idle prompt after the twins land already runs them), builtin-only and fork-free (measured 2026-09-16: zsh ~54 µs, bash ~135 µs, fish ~22 µs per call; a fork is ~1300 µs).
~/.aterm/shell.d/00-atpkg.<shell>is (re)sourced live whenever the copy on disk is not the copy last sourced — it appeared, or atpkg rewrote it — in EVERY shell: zsh by onezstat(mtime:inode), bash and fish by the hook's TEXT taken in with thereadbuiltin on a builtin redirection and compared to the text last sourced (bash has no fork-free mtime; a-ntstamp file would be per-shell state to mint symlink-safely, own and clean up — rejected, the reasoning is in the scripts). A pre-R1 hook is sourced once per copy, not per prompt; an ABSENT hook never returns early, so an in-session PATH prepend (. ~/.cargo/env) can no longer shadow the reroute stubs in bash or fish (audit finding — only zsh held before).$ATERM_REROUTE_DIRis derived besideagents/for a shell that predates it unlessATERM_NO_REROUTEis engaged, a dir the hook names before atpkg creates it is re-probed until it appears, and zsh rehashes when theagents/listing changes while bash forgets the two managed names (hash -d) — a twin landing in a dir that already led PATH used to lose to the hashed foreign copy for the life of the shell. Gated on being inside an aterm session; the zsh block runs underemulate -L zsh; bash is nounset-clean (under a user'sset -uthe prompt command used to abort on an unbound one-shot flag at every prompt, leaving the DEBUG-trap preexec — the per-command re-assert — dead for the life of the shell; every reference is${…:-}-guarded now, and zsh's loader guard and first-precmd one-shot aresetopt nounset-clean too). Real-shell tests replay the owner's scenario in zsh, bash and fish (foreignclaude→ hook and twin laid while the shell sits idle → managed build 1 → re-laid → build 2 → rewritten hook re-sourced, all three shells), the bash scenario again underset -u, and — no hook at all — a prompt-time PATH prepend over the seam's reroute dir in each shell (cargostill runs the stub), plus a fork-free grep, wiring pins and an atpkg-side golden pin of the hook format (crates/atpkg/tests/shell_integration_hook_pin.rs); the fish lane runs underTERM=dumb(fish 4.9.3 waits 10 s for a Primary Device Attribute reply under a terminal-class TERM). The tab the owner was looking at still runs the pre-2026-09-16 script (the installed copy is rewritten only when a new shell spawns): there,. ~/.aterm/shell.d/00-atpkg.zshpicks the managed copies up in place — verified on that machine — and every shell spawned by this build, and every tab adopted after it, stays live on its own. - Window:
<prefix>/agents/is created at launch (onemkdir, never a wait) and front-inserted on every window session's PATH — the very first tab runs the managed build the moment atpkg lays the twin. The status row reads "whatclaudeandcodexrun in every aterm tab, this one too · builds …", and says so on the pill's evidence: a window whose shell integration failed reads "… — but this shell isn't hooked up to them; see Settings ▸ Packages" instead. A tab adopted across a seamless update from a build before this one keeps the PATH it was born with: the handoff manifest carriesoutgoing_build(absent ⇒ the old build wrote it), such live tabs are counted — pending adoptees included, so the row is right the first time (the atpkg launch pass printsmanaged-current:before the first park adopts the handed-off shells; the restore re-runs the last text and posts only a changed count) — and the row says "… · N tab(s) from before this update pick(s) them up with. ~/.aterm/shell.d/00-atpkg.zsh" (spelled for the spawn shell byHookDialect::remedy, the one source the pill shares). Sourcing the hook heals PATH in place AND keeps the tab's shell integration;exec $SHELLinside an aterm tab drops it (the zsh wrapper consumesATERM_ORIGINAL_ZDOTDIR; bash rides--rcfile), so no surface names it. The installed pill reads "ready in every aterm tab, this one too" (the hook appendsbin/unconditionally, so the ALab tools are live in the same tab as well), narrowing to "ready in every tab opened since this update · 1 tab from before it picks them up with…" while tabs from before the update are alive (joined with the row's·so a cut keeps the qualifier with the command; pinned at 80–160 cols). Documented bounds: the frozen count is lowered by a tab closing and by nothing else, and a rollback to a pre-field build followed by an update marks every handed-across tab frozen — accepted as an upper bound. A WSL tab keeps "a new tab picks them up": its bash sources the distro's$HOME/.aterm/shell.d, where the Windows-side atpkg writes nothing. - TTY session (
atermin another terminal): the seam front-inserts the same two directories BEFORE the rc files, as a pre-rc hint — that lane spawns a login shell with no aterm shell integration (neitherATERM_CHILDnorATERM_SESSION_ID), so on macOS/etc/zprofile'spath_helperrebuilds PATH ahead of them (measured 2026-09-16: positions 12–13, behind/usr/local/binand/opt/homebrew/bin) and what putsagents/first at that lane's prompt is the rc-sourced hook, once, at shell start (pinned by a real/bin/zsh -l -itest).$ATERM_REROUTE_DIRis taken only as an absolute existing directory (a relative stray no longer creates./agentsin the cwd); the session'sagents/mkdir followsLayout::ensure_dir's rule from the prefix's own metadata (0700in a prefix we own,0755in a root-owned system prefix, refused in anyone else's), refuses a symlink or non-directory there, and its stderr line namesaterm pkg repair(which re-lays the directory) — as does the window's. Documented residual: under--no-reroute/ATERM_NO_REROUTEa TTY session hands nothing unless an enclosing shell exported$ATPKG_AGENTS; the rc hook covers it. - atpkg: for the agent programs
doctor,whichand the pass log say what is true of THIS shell — "SHADOWED in this shell by <path>: its PATH has no <prefix>/agents (a shell that has not sourced the atpkg hook) —. ~/.aterm/shell.d/00-atpkg.zshhere picks the managed copy up; a tab opened on this build puts agents/ first on its own" — never "remove or reorder", never a new tab. The remedy is CHECKED per shell (the hook where it exists, aPATHline in the shell's dialect otherwise;shgets the line, the bash hook is not POSIX sh) and keyed on the shell that TYPED the command, not$SHELL(atpkg::caller_shell::invoking_shell: the parent process's executable —sysctl(KERN_PROCARGS2)on macOS,/proc/<pid>/common Linux, no fork — because aterm spawns the configured shell without re-pointing$SHELL, so a fish tab on a zsh-login machine was told the zsh line). The pass'smanaged-current:line (the "aterm-managed, current" row) is printed only when the front-of-PATHagents/twin really resolves to the active build.
- Shell integration (zsh, bash, fish; pwsh best effort): the managed dirs are re-asserted at EVERY prompt and EVERY command (preexec, so a command typed at an idle prompt after the twins land already runs them), builtin-only and fork-free (measured 2026-09-16: zsh ~54 µs, bash ~135 µs, fish ~22 µs per call; a fork is ~1300 µs).
- A
claude/codextyped while a newer build is landing says so on stderr and runs the new build when it lands (owner: *"when we are waiting for latest updates, have an stderr warning about waiting for the update"*). The update pass marks an in-flight agent-program landing (<prefix>/landing/<program>, inside the store lock, gone the moment the install ends either way;0600under a$HOMEprefix,0644under a system prefix where root's pass writes it and every user's twin must read it); theagents/twin's prelude hands over toatpkg __landing, which printsatpkg: waiting for the claude update to land — 2.1.274 (build …), downloading 42% (12.3 of 29.0 MB) — Ctrl-C runs 2.1.273 now, refreshed every 2 s, bounded byATPKG_LANDING_WAIT_SECS(default 45; 0 = warn once), then runs the NEW build oncebin/<program>resolves into it — or the build you had on expiry, Ctrl-C or a failed landing (… did not land (<why>) — running 2.1.273 now;aterm pkg updateretries it). Each tick reads the marker FIRST and the shim SECOND, so a landing whose activation and guard-drop fall between two looks is reported as the landing it is; the verb follows a marker another live pass laid over ours, and a stale marker is removed only while the file still names the dead pid. The twin never strands the tool: with the embedded atpkg gone it falls through to its storeexecinstead of an olderatpkgon PATH. The Windows.cmdtwin carries the same wait since 2026-09-17 (unverified on a Windows host; see the Changed entry).aterm help pkganddocs/DESIGN-which-copy-runs-2026-08-27.md§5 carry the guarantee and the knob. - Cursor-trail sound: the bare Shift is a "ting". Owner: *"there is no 'shift' tone for the rainbow cursor trail, it is supposed to be a 'ting' or something musical to complement the space bar"*. It was a whisper-level pickup — on the v1 palettes a 100 ms soft sine at the ladder's quietest tier, on the music box an 80 ms lift measured −6.1 dB re a letter — thinned by the keystroke governor on up to 29 of 34 presses at typing speed (the host mints Shift 30–100 ms before the capital) and, under the music box, cut 60 ms in by the capital's own graft. Now a struck-glass bell in its own bright register (E6..E7) at the Space's tier in every voice — with its own 75 ms gate (
SHIFT_MIN_GAP) on the v1 palettes, and its own mono voice lane (LANE_TING, cap 1) under the music box; it still rotates through five pitches, never steps the melody and never claims the keystroke beat. The audit refit its v1 level (0.27 → 0.33) to the WALK-MEAN letter — the one-degree fit read −7.2 dB on Comet, the whisper the owner reported — and on a ghost slot of the bar the ting wearsSONG_GHOST_LEVEL, so it never out-peaks the capital it announces (was up to +9 dB over a mid-word capital on Comet). Measured on the bench: −2.4 dB re the walk-mean letter (music box), every v1 voice inside −3 ± 1.5 dB, pitched, f0 ≈ 1.5 kHz, ringing ~220 ms, 0 Shifts silenced on every census row. - Cursor-trail sound: a shifted glyph is louder as well as higher — everywhere. Owner: *"make shifted keys higher in pitch and louder"*.
SHIFT_GLYPH_GAIN(+2.6 dB) on every shifted glyph (the Typed kind — a shifted Space, Enter or arrow is still what it was) in every voice; under the music box a capital is never a passing tone in LEVEL, so a capital inside a word (camelCase, an acronym) gains the same weight as one opening a word — the first cut measured +0.4..+0.8 dB mid-word because the lifted degree tookPASSING_LEVEL; and on the v1 palettes the capital's ROOF follows its note (TrailSynth::shift_octave, thenshift_roof, lifts every tonal voice'slp_cutby the octave), because the octave lift used to walk the note up a fixed roof and the weight collapsed to +0.8 dB at other degrees of the walk. Independently re-measured over nine seeds and the walk: music box +1.8..+5.2 dB (the parent build made capitals 2 dB QUIETER), Lumen +2.3 minimum. The octave lift on the v1 palettes is unchanged; the music box's 2026-09-09/10 pitch law for capitals (three degrees where a word opens, plus the scoop) is not re-opened here. - Cursor-trail sound: the single space bar is heard. Owner: *"i don't always hear the space bar?"*. The census proved the governor thins no space head a hand can produce (0 of 60–91 at 6–10 cps), so the cause was the music box's downbeat itself: a dark ~300 Hz dyad measured −8.7 dB under a letter with 0.000 of its energy over 2 kHz — a spectrum a laptop speaker rolls off. Re-ruled over the 2026-09-10 "felt, not heard" fit with the owner's words: the dyad at −4 dB re the step (
BASS_LEVEL) and the head's breath carrying the chord root's twinkle in the stardust band (v1's own 2026-08-30/31 repair on the music box's lattice). Measured after: −4.5 dB re a letter, centroid 510 Hz, 5.7 % over 2 kHz — heard beside the letter, never over it (the ladder's ceiling still holds). Found on the way and fixed: every music-box BREATH voice (the space's exhale, the run tail, the stop's late breath) had been rendering SILENCE since 2026-09-06 — built without a low-pass roof, and a roof of 0 is a filter coefficient of 0 (BREATH_ROOF_HZopens it) — and the meteor's TICK was built the same way and rendered silence too: onemet_tick()prototype underMET_TICK_ROOF_HZnow serves both sites (it renders, at −59 dBFS alone; the engine's 5 ms anti-click ramp over a 4 ms voice keeps it far under the step, recorded indocs/design/RAINBOW-KITTY-V2.md§34.5 for the owner to rule on). - Cursor-trail sound: the same-day audit follow-up. A shifted capital's +2.6 dB weight now survives every v1 palette's own lottery: the octave lift had doubled the beat rate of a palette's detuned chorus twin (Comet ×1.006, Lumen ×1.004), so the capital sat at a different point of the beat than its plain self and read as little as +0.9 dB over it on Comet;
TrailSynth::shift_octave(wasshift_roof) now keeps any twin withinDETUNE_MAX_RATIObeating at the plain letter's rate — Comet reads +2.5..+3.6 dB on the crest over 260 walk readings and every v1 voice ≥ +2.3 dB on the strike's energy, pinned on five seeds. The meteor's tick is audible at §12.2's −24 dB re the step (was −40: its 4 ms life sat inside the 5 ms release ramp) — its life isRING_OUT_PER_TAU× τ plus the ramp (12.5 ms) andMET_TICK_LEVELis fitted to what it renders, median −24.0 over ten seeds. The bare Shift's ting decays out instead of being cut: 430 ms (was 260 / 230), reaching −40 dB re its own peak at 381–407 ms, before the ramp. Thekeyboard_song_abspace headverdict reads the Space against the walk-mean letter — thetingline's own instrument — so a v1 palette's walk no longer decides the verdict by seed (Comet's own space voice still reads "no top", a pre-existing property of that palette). Design record §34.6. - Cursor-trail sound: the v1 space head is heard with top at every root of the walk (2026-09-17). The bench's
space headverdict (owner, 2026-09-16: *"i don't always hear the space bar?"*) read NO TOP on Comet (centroid 420 Hz) and Laser (440) and green on the other ten v1 voices — and the cause was the ROOT, not the palette: the head's twinkle rode 16× the walk's root, the two A anchors fold their first root to the floor of the [220, 440) register, and every voice's walk visits that floor on later words. Over the engine's 432-reading sweep (12 voices × 12 roots × 3 seeds) 43 readings on nine voices read NO TOP. The twinkle is now folded by exact octaves into one sparkle band, [4.4, 8.8) kHz (SPACE_TWINKLE_LO_HZ,twinkle_hz) — still the root's own power-of-two pitch class — and the chime pair is lifted +2.1 dB (0.11/0.07 → 0.14/0.09); the head's peak moves under 0.3 dB on every voice. Bench, before → after: Comet centroid 420 → 575 Hz, Laser 440 → 596, Lumen 585 → 625, Sparkle 500 → 657, Marimba 473 → 633; every v1 voice reads "heard beside the letter, never over it, with top"; the music box is unchanged. The verdict's three clauses live once in the engine (SPACE_HEAD_RE_TYPED_DB,SPACE_HEAD_CENTROID_FLOOR_HZ,SPACE_HEAD_HI_FLOOR, f64 so the bench prints0.02and not a widened f32), read by the bench and by two new pins (the_space_head_is_heard_with_top_on_every_v1_voice: every v1 voice, every root of the walk, three seeds — lowest centroid 510 Hz, lowest over-2 kHz 0.029, walk-mean tier −5.7..−1.5 dB, a stated −8.0 dB per-reading floor for Fire's crackle lottery;the_twinkle_is_the_roots_pitch_class_in_one_sparkle_band). Ladder law, walking bass, the monophonic damp and every golden untouched (the v0.56 oracle never pushes a Space). - Three tests stop failing on an Intel Mac for reasons that were never the code under test. The certificate differential recorded
acceptfor github.com's leaf with only the intermediate it truly needs, anchored by Sectigo Public Server Authentication Root E46 — which the recording machine's store carries and macOS 13.7.8's does not (it anchors the USERTrust root the withheld cross-sign leads to): ours saidUnknownIssuer, the platform said-67843naming E46, and both tests went red on a machine whose verifier was right. Such a row is now held to what the store can decide — both sides reject, twice the same, and the chain with its cross-sign accepts on both — and printed as aSKIP, the way the file's anchor probe already skips a chain a machine cannot validate; every other row is still asserted for equality and for its accept. The no-ack broker test measured its 5-second deadline from the first poll that saw the attach, 1.149 s behind the stub's own accept on a loaded 4-core box; it measures from the accept now, bounds unchanged.
- The byte-exact Over parity sites report a one-LSB miss on x86_64 macOS instead of failing there, and still fail past it. Six sites in five tests — the ink-fire Over, the Over veils, the damaged-path Over veil, the source-over
glow_underfield and theribbon_beam_vtrain (full-frame and damaged-path) — demand CPU==GPU to the bit for the source-over streams, on the strength of the shader's "one rounding on store, no exact ties" argument, measured on Apple silicon GPUs. On a 2017 15-inch MacBook Pro both GPUs store the same source-over one bit off on some pixels (measured 2026-09-16, macOS 13.7): the Radeon Pro 560 misses on every row the four original tests reach, and on the Intel HD Graphics 630, the low-power pick the Metal comparison suites need, which rows miss varies from run to run — always by exactly 1. The sites now go throughassert_byte_exactin the parity kit, thearm64_pinpattern of the effects crate: everywhere but x86_64 macOS it is the sameassert_eq!(delta, 0), and there a one-LSB miss prints one stderr line naming the adapter while a delta of 2 or more fails exactly as before. The additive streams' ten exact sites (a raw 8-bit add, exact on every GPU) are untouched, and the downlevel gate still comes first.
BOOL *encodes^con x86_64, and the SDK-constant oracles report the rows this host's clang cannot fold instead of skipping whole tests. The runtime census sentsend_boolasB@:on every arch where the runtime answersc@:on x86_64 (one disagreement on an Intel Mac, 2026-09-15), and the two SDK oracle tests failed on Apple clang 14.0.3, which cannot fold the sixstatic constrows (NSNotFound,NSModalResponseOK, …) into_Static_asserton either-arch. The encoding now comes from the crate's ownBoolon every arch. The oracles keep asserting every enum row on x86_64 macOS and print one line naming the rows the compiler could not fold, and the inverted arm passes only when clang fails the flipped row for its value, never merely because it could not fold; every other target asserts exactly as before.
- The Metal FFI spells ObjC
BOOLat its own width, and every texture it mints isManagedby name. Twelveobjc_msgSendprototypes in the Metal FFI wrote Rustboolwhere the selector's type is ObjCBOOL, which issigned charon x86_64 (256 values) and_Boolonly on arm64; aboolthere isi1 zeroext, so any byte but 0 or 1 crossing it is undefined behaviour, and a probe on an Intel Mac showed three setters handing a 3 straight back. Two more reads went through a hand-rolledu8(the width, not the sign). All fourteen now go throughObjcBool—boolon aarch64,i8elsewhere — so arm64 codegen is unchanged. Andnew_texture_2dnever set a storage mode while seventeen comments downstream called the result "shared": measured on both GPUs of that Mac, the default isManaged(Shared is refused for textures), so the descriptor now saysManagedexplicitly and the comments state the contract that is true —replaceRegion:is legal, CPU reads stay a blit into a Shared buffer. Three probe programs undertools/metal-storage-proberecord what was measured.
- A dual-GPU Intel Mac renders on its low-power GPU, and both render arms use the same one. Every Metal device the crate opened came from
MTLCreateSystemDefaultDevice, which on a 2017 15-inch MacBook Pro is the discrete Radeon Pro 560 — so the app kept the discrete GPU powered for a glyph atlas the Intel HD 630 draws just as well, and the Metal-versus-wgpu byte-for-byte tests compared a Radeon render with an Intel one (the wgpu arm already asked for the low-power adapter): 19 of them failed by one bit per channel on that Mac (measured 2026-09-15).Device::preferrednow picks the first low-power, non-headless device unlessATERM_GPU_POWER=high, the same policy as the wgpu arm'spower_preference_from_env; the production renderer, the Metal arm, the blit and every test harness open the device through it, and the bundle declaresNSSupportsAutomaticGraphicsSwitchingso macOS may switch the mux back. On an Apple silicon Mac there is one device and the pick is unchanged.
- The merge gate's atpkg publish-tooling row now builds the
atpkgit drives, and can no longer pass on a stale one.tools/verify.sh --fastrunstools/test-atpkg-pack-one-compiler.sh, whose last section packs a real sysroot bundle with a realatpkgbinary — resolved as$ATPKG, else<repo>/target/debug/atpkg, else the release one — and on a host that can run that pack (macOS with cc, codesign and zstd) a missing binary is a gap in the run rather than a platform limit, so the suite fails instead of printing PASS over half its checks. The row was a pure stage that started at t0, before anything had built that binary: measured on 86381efbf it ran 2.685–112.393 s and failed with "section D (the real pack end to end) cannot run: no atpkg binary", while the workspace build that writes that path ran 2.685–805.370 s. Three earlier gates had passed the row only because a staleatpkgfrom an earlier run was still lying in the warm snapshot, so on a cold checkout the row could never pass. It is now a driver-lane stage, printed after the sealed fabric rung and before the smokes: it builds-p atpkgintotarget-drivers/first, runs the pack suite only if that build succeeded, and hands the suite$ATPKG= the binary it just built, which puts the stale-fallback path out of reach. The two self-contained suites beside it are unchanged and still run either way. Contributors who run the gate on a fresh checkout see this row pass for the first time. - atpkg no longer hard-links the Trust toolchain into anything, and
aterm pkg repairnow actually makestippyrun. The owner: *"tippy is broken? fix aterm pkg repair so that it DOES FIX! … Trust and rustc are different … hard linking? that sounds stupid … doing that with hardlinks sounds like bugs and indeed: bugs."* On the owner's machinetippy,targo tippyandtargo-tippystopped attippy: setup error: rustc-compatible sibling …/bin/rustc is not the selected Trust compiler …/bin/trustc, and 0.86.0'saterm pkg repaircould not fix it: the installed Trust bundle 8595 shipsbin/rustcas a separately signed copy its own tippy refuses, Trust fixed that at the source on 2026-09-14 (tippy runstrustcunder its own name, and the distribution ships Trust's names only) but no Trust build has been published since, and 0.86.0's atpkg had no remedy at all. Both remedies atpkg grew since — the rustup view and the per-build exec roots — were HARD-LINK mirrors of the store's own inodes, so every lay, rebuild or retire moved a live store file's link count and ctime (which aborted every tippy running from the store) and a link from a tracked process tagged the store itself. Both are now copy-on-write CLONES: a new inode per file, the store's bytes shared by APFS, the store's link counts, ctimes and attributes untouched — bundle 8595, 4,114 files and 2.6 GB, cloned for about 2 MB.rustc,cargoandrustdocin a view or root are clones oftrustc,targoandtrustdoc, which is all 8595's tippy asks. Measured on bundle 8595 itself: its tippy lints from the clone (a planteduseless_veccaught),trustc --print sysrootanswers the clone, and the same crate from the store still stops at the setup error. A view file matches only when it has its store file's length, mode and time AND a different inode, and repair and doctor also read the three stock names' bytes (8595's refused copy hastrustc's length, mode and time), so every view or root laid as hard links is rebuilt once by the nextaterm pkg repairand the store's link counts come back to 1. The routed shim's-efsame-inode guard, which only a hard link satisfies, is replaced bytestbuiltins that take the root only when its file is a regular executable, the store file still exists and the root's marker (written last) stands — the same decision under/bin/sh, zsh, bash, ksh and dash across nine shapes, measured.aterm help rustreads the new guard. A clone never crosses a volume: atpkg refuses rather than byte-copy a toolchain. Elsewhere than macOS the same call reflinks where the filesystem can and copies otherwise, which costs the rustup view's size on a Linux ext4 machine where hard links cost nothing; no Linux bundle needs an exec root. - A bundle that failed to extract said neither which member nor why. Every disk error inside the extractor —
create_dir_all,open,write,set_mode,hard_link,symlink, the content read, the mode read-back — went intoExtractError::Iothrough a bare?, so atrustbundle whose disk filled at member 300 of 508 reported exactlystage: extract: io: No space left on device (os error 28): no member, no destination, no read-vs-write, and the partial tree is deleted on the way out, so nothing survived to say how far it got. The same bare line covered a macOSEPERM, which is usually privacy consent arriving with no dialog — where the operator's only question is *which path* needs it.Ionow carries the operation and the path (io: create_dir_all "/…/incoming/bin": …). Alongside it, seven unrelated refusals shared the one message "bundle exceeded the signed size/entry cap": the byte cap, the entry cap, the per-entry structural budget, a directory / hardlink / symlink member declaring a nonzero body size, and a zip symlink target over 4096 bytes. An upstream tarball that puts a size on a directory header — some old tar writers do — was refused with a size-cap message although its 30 MB archive was nowhere near the 200 MB cap, sending the operator to chasedisk_installedinstead of the offending header.ExtractError::TooLargenow carries aTooLargeReasonnaming which defence fired, with the raw member path on the header-shaped ones. No refusal changed: the same bundles are accepted and the same ones are refused, they just say what happened. - The proof-cache guard accepts atpkg's Spotlight-noindex
targetlink.aterm pkg's[machine] spotlight_noindexpass (0.86.0, on by default) renames a git checkout'stargettotarget.noindexand leaves a relativetarget -> target.noindexsymlink, because the checkout's tracked.cargo/config.tomlmay not be edited.tools/proof_cache.shandtools/proof_cache_selftest.shrefused any symlinkedtargetoutright, so on every machine the app had migrated theproof_cache,paint_guardandspin_guardteeth ofxtask gate lintanswered "the Python aggregate lock could not be acquired safely" on every run (measured 2026-09-16, in the terminal and under an untracked launchd job). One helper,_pc_target_dir_is_usable, now admits exactly that shape — a relative link namedtarget.noindexwhose referent is a plain sibling directory — and still refuses every other link, a file, or a link to a link. The same shape is admitted at the two other places that refused it: the build lane'sproof_guard_target_acquireand the guards' state-directory parent (_pc_prepare_state_dir, "the durable attempt marker could not be written"). xtask gate lintlaunches its guards throughenv bash, never by exec'ing the script file. On macOS a process is provenance-tracked when the executable it was exec'd from carriescom.apple.provenance, and a script exec'd by path counts as that executable — the ruledocs/RELEASING.mdrecords for shims. A checkout written from a tracked shell (every shell inside aterm.app, every agent under a taggedclaude) has every tracked file tagged, so exec'ingtools/spin_guard.shtracked the guard, the guard tracked its take, andproof_snapshot.pyrefused it with "published proof snapshot has extended metadata" even from a launchd job, the one untracked lane there is. Measured 2026-09-16: exec of the tagged script → tracked;bash <script>andpython3 <file>→ untracked. The guards are#!/usr/bin/env bashand resolve themselves from$0, which the interpreter launch preserves, so nothing else changes.- The artifact fingerprint asks a tool's version through the name it found, not the resolved file.
_executable_identitycanonicalized the discovered path before running it, which defeats every multicall binary that dispatches onargv[0]: rustup's~/.cargo/bin/cargois a symlink torustup, so the snapshot pass'scargo --config <sealed-registry> -Vvran asrustup --config … -Vv, which is "error: unexpected argument '--config'", exit 1 — on stdout, so the guard reported "cargo version exited 1 without a diagnostic" (measured 2026-09-16, the first time a paint/spin take materialized on a rustup machine). The identity still hashes the resolved file's path, mode and bytes; only the verdict is asked through the name._runnow quotes a refusal printed on stdout when stderr is empty. Two tests pin both. - A hardlink whose target was named THROUGH a symlink was physically linked before anything refused it.
Layer::hardlinkvetted the link's own path against the symlinks already laid, but the target got only the lexical component walk: bothsymlink_metadataandhard_linkdecline to follow the LAST path component only, so a vendor archive namingalias -> lnk/fafter layinglnk -> realhadaliascreated on disk as a second name forreal/f, and was then refused — with a misleadingHardlinkTargetMissing— only because the digest fold could not findlnk/famong the paths it had written (in the write-only lane, which folds no tree, nothing refused it at all; that lane admits no symlinks today, so this was latent there). The target's ancestors are now walked by the same guard as the link's, so the vet refuses it asThroughSymlinkand the alias is never created.
- Stopping a live pass's progress threads no longer costs the pass thread a tick of idle time. The
.partdownload poller and the pass heartbeat each ended their loop body in an uninterruptiblesleep(100 ms and 500 ms), and each is stopped by raising a flag and JOINING from the pass thread — the one holding the store flock. The flag could not cut the sleep short, so everydrop(download_watch)sat out the rest of the poller's tick between download-complete and the sha256 (up to 100 ms per downloaded artifact: a default set pays it a dozen times), andend_passsat out up to 500 ms before writing the terminal snapshot the GUI retires its progress row on. Both ticks now park instead of sleeping and the stopper unparks them, so the join returns at once. Tick cadence, the stop-before-snapshot ordering, and the stop-the-poller-before- the-phase-moves-on ordering are all unchanged.
- A live install pass no longer costs a process spawn every two seconds to answer a one-syscall question, and a pass owned by another user no longer reads as dead. The liveness half of the
progress.jsonstaleness gate (snapshot_running) forked/bin/kill -0 <pid>and waited for it. That probe runs on every pending-program stub invocation while a pass is live, and — through the GUI tailer's foreign-pid probe — once every 2 s for as long as ANOTHER installer holds the store lock, so a multi-GB pass queued behind--wait-lockfor 40 minutes spent ~1200 fork+execs on a GUI worker thread to learn whatkill(pid, 0)returns in microseconds. The stated reason for the subprocess (keeping atpkg's non-test codeunsafe-free) had already lapsed: the same file callslibc::poll/libc::dup2, and the crate's staging lane already probes a pid withkill(pid, 0). It is now that one syscall — which also answers the question the gate actually asks, "does this process exist": a pass running as another user (undersudo, say) is no longer misread as a crashed installer the moment/bin/killexits non-zero onEPERM.ESRCHis the only "no", and every other errno still falls back to "alive" behind the 10 s heartbeat window.
- A typed
aterm pkg update <program>no longer re-downloads an index it can prove it already has.plan_update,apply_programandrollbackresolved their candidates with a direct fetch. That was right about the §14 cache's failure-time fallback — a transient index fetch must fail, not install from a cache the network could not corroborate this pass — but it also skipped the cache's identity HIT path, which is not a fallback at all. So every typed single-program update or rollback, in a fresh process, re-fetched all four assets of each of the four candidate releases (sixteen sequentialcurlsubprocesses, each paying its own DNS+TLS handshake) to obtain the very bytes the 6-hourly pass had just stamped as identical in<prefix>/index-cache.toml. These lanes now ask the cheap question first, off the release listing they were going to fetch anyway, so a matching identity costs no downloads and no extra request. The failure rule is unchanged: an unreachable listing answers no identities, the cache is refused, and the fetch still fails intoNoIndexwith no cached fallback. - The rainbow trail no longer loses cells out of the MIDDLE of a lit line when a program changes one glyph under it. The owner on 0.86.0: *"there is still this gapping issue that arises in codex, it seems to happen when doing backword and editing"*. The trail's content witness retires light whose text has been rewritten underneath it, and it took that verdict one cell at a time: a single interior cell whose glyph changed, or went, was melted alone — a one-cell hole in a band whose letters stood lit either side — and a single interior cell whose glyph went and stood nowhere else RELEASED its whole run, abandoning the cohort, so the band drained under a hand that was still typing. Any program that paints into a lit line does this: measured with real Codex 0.154 in a headless instance — its composer paints an ambient field of dot particles into its blank cells and clears the one under the caret — the owner's gesture (type a line, Alt+Left ×3, type a word, Alt+Right ×3, type two letters) left one-cell holes exactly on the spaces the hops had parked the caret on, 73 of 295 band frames broken on HEAD and 47 of 295 on 0.86.0, and the band fell from 33 lit cells to 9 within 600 ms of plain typing where zsh keeps 38. A spinner glyph, a spellcheck rewriting one letter, a TUI drawing its own cursor glyph would do the same. The witness now reads its names as ONE SHAPE against the run they stand in — the law
retract_suffixalready states for every other path, *"CONTIGUOUS at every frame … nothing is ever removed from the MIDDLE"*: names that reach the run's first or last recorded cell are a prefix, a suffix or the whole and stand; names strictly inside that cover most of the run are a rewrite whose end cell happened to compare equal and are extended to the nearer end; names strictly inside that cover less are not evidence, the light stays and the records take the glyphs standing there now. And a release that names only part of a cohort's cells no longer abandons the whole cohort: the named cells leave the way an erase's suffix leaves — stamped onto the retract, counted once — the cohort's clock is untouched, and the identical redraw a frame later lifts exactly those stamps again (an Ink-style composer that blanks a tail and puts the same text back keeps its light); only a release that names every standing cell is the text gone whole. Codex's exact recorded bytes are replayed at the host seam by a new test, red on both counts before this; the composer, scrub and wrapped-band suites are untouched, and a new host-seam suite pins the partial release's four shapes (a cleared tail leaves and is counted, a whole clear after it still swooshes, the identical rewrite restores, no light stands ahead of a typing hand). - A key typed into a lit line gets its own cell. The echo's sweep laid a held key's cell only where no live cell owned the column, so the first key of a mid-line insert — typed over the band the caret had hopped back onto — laid nothing; the old cell under it kept the old glyph's identity, the witness read the new glyph as replaced text and melted it with the shifted suffix, and the insert column stood dark for the rest of the line. Measured on glass with real Codex: one dark column exactly where the word hops had parked the caret, for 2.5 seconds. A live cell born before the key is an earlier key's glyph the hand typed over and is re-laid as this key's own; a cell born at or after the key is its own and is left as it was.
- A word hop across an edited line no longer leaves a seam that opens into a hole as the trail fades. A mid-line insert retires the light under the text it shifted; a word hop across that dark stretch lays a wake, and a key typed at the hop's landing joined THE WAKE, so one contiguous band was two cohorts, each draining farthest-first over its own span when the hand rested — a hole opening at the seam and widening to three cells while the band to its left still stood. A typed key now folds every wake at the cell it lays into the typed band it belongs to — from either side, since a landing inside the line's old bounds (which never shrink when cells are retired) used to hand the key the typed cohort and leave the wake to expire under the typing hand,
###########..########540 ms after the hop — priced like the band's newest glyph, so the row keeps one cohort and one drain through the fade. A band already reaching, retracting or fading is not taken back by a key in a wake beside it (the fold plus the hold would have lit its spent cells to full in one frame), and the band's cells under a folded wake go with it, so no column carries two owners of one cohort. - A tagged reroute marker is re-laid by
aterm pkg repair, as the stubs beside it are.<prefix>/reroute/.atpkg-reroute-diris written by the same untracked lane as the eight reroute stubs, mode0755, soaterm pkg doctorcounts it among that directory's executables and namedrepairas the fix for a tagged one — andlayonly ever wrote the marker when it was ABSENT, so a marker laid in-process from a tracked app before the lane existed stayed tagged through every repair, under a warn whose remedy could not clear it (1 of 9 shim(s) in …/reroute carry com.apple.provenance (e.g. .atpkg-reroute-dir), measured through three repairs on 2026-09-16). The marker now follows the stubs' own rule (reroute::marker_needs_lay): laid when absent, re-laid when it carries the tag and this pass can lay it clean, left alone otherwise; the rewrite is the lane's temp-and-rename(2), so a stub's escape walk never sees the directory unmarked. - The index walk no longer buys a metered listing page whose releases nothing can select. Resolving the signed index uses only the newest
INDEX_CANDIDATE_CAP(4) releases carrying a complete authorization quad, but the release walk always read the listing to its end. When the shared app repo crossed 100 releases (117 on 2026-09-16 = two pages) with all fouratpkg-index-*candidates still on page 1, every pass on every machine started spending 2api.github.comrequests where 1 answers — and 3 once the repo passes 200. That falls on the lane with no budget to spare: the advertisedcurl … | bashinstall is anonymous, capped at 60 requests/hour per IP, so a shared office NAT paid the extra request once per machine per pass for rows index selection can never choose. The index lane now stops at the page that completes the candidate cap, and still pages on when it must (a sparse index history, or a burst of app releases pushing the index tags down) — the pushed-off-page blindness the paginated walk exists to close is untouched. The listing memo keeps that prefix marked incomplete, so a prefix is never served as the whole catalog and the manifest/artifact fallbacks, which do need it whole, resume the walk after the prefix instead of re-reading the pages already paid for.
- A bundled display face no longer loses its fitted grid the first time the config is saved or the theme flips.
display_fontpicks a bundled display face, and FONT-DISPLAY-FIT measures that face's widest glyph so no character paints over its neighbour — the "m ncraft" overprint. The lookup that decides whether a face takes the fit matched the registry by POINTER, so it recognised only the one copy of the bytes the registry itself holds. Every rebuild path rebuilds from a copy, so the config/theme hot-reload (rebuild_from_admitted, and the semantic fork beside it) silently dropped the WHOLE policy — the scaled rasterization size, the widest-advance cell, the ink centring and the embolden headroom. Measured forengravedat 16 px: the cell went from 21 px to 15 px while the widest glyph still rastered at 24 px, nine pixels of ink over the cell edge in every column. The fit is now a property of the FONT — matched by content — so a copy of a face is the same face. - A
font_weightorfont_variationsetting no longer collapses that same grid at startup. Re-resolving a variable font's instance re-derived the cell with the one measure that ignores the display fit, and never re-measured the new instance. Because the font config is applied immediately after the renderer is built, this fired on the one generation that actually carries a fit: cell 21 px against a 22 px glyph. It also left the pure metrics read reporting 21 px while the renderer painted 13 px, which put grid sizing, hit-testing and IME positioning eight pixels per column out of step with the glyphs. Both now agree, because both go through the fit. Swapping the primary face re-measures it too, rather than inheriting the fit of the face it replaced. aterm ctl invokeno longer answersOKfor a menu action that declined. A menu press has nowhere to print, so these actions were written to do nothing quietly — correct for a menu, wrong forinvoke, which mints a reply a script reads as "the action happened".FavouriteKittyandNextKitty(with effects or the feline family off, or nothing collected),MoveTabToNextWindow(one window),ToggleFullScreen,MinimizeandZoom(no window on screen), andFindNext/FindPrev(no search open and nothing remembered) all reported success while changing nothing. Each now answers with the reason it declined, named for the state the caller is actually in rather than one shared apology.
- A cache write that cannot land no longer strands its temp file at the prefix root.
IndexCache::storewrites<prefix>/.index-cache.tmp-<pid>and renames it overindex-cache.toml, but a failedfs::writereturned at once and a failedfs::renamewas discarded with a barelet _— both leaving the temp on disk.fs::writecreates the inode before it fills it, so a full disk (ENOSPC/EDQUOT) stranded one on every resolve, and because the name carries the PID each pass stranded a *distinct* file. Nothing swept them:gcwalksstore/andstaging/, never the prefix root, so they accumulated for the life of the prefix — precisely when an operator is hunting for space, andaterm pkg doctorreported nothing to reclaim. Both failure paths now unlink the temp, as the progress-file writer already did. The cache write stays best-effort: a failure still never fails an install.
- A
--progress-filewhose first write fails no longer starts a live pass that writes nothing.ProgressSink::createpromisesNonewhen the destination is not a regular file *or the initial write fails*, but it only consulted the non-regular-file flag: when the GUI named a progress file under a prefix that does not exist yet (or one this user cannot write), the write failed, nothing in the sink's state changed, and a live sink came back anyway.begin_passthen reported an OWNED pass (true), installed the process-global sink, and spawned the pass heartbeat thread — so for the whole install the heartbeat ticked and every.partdownload poller ran, each retrying the same failing write, against a file that could never appear; the network and sealed-seed lanes both ran "under" that phantom pass.createnow returnsNoneunless the first write actually landed, which is also what the seed lane's rule ("a sealed seed whose ownbegin_passfailed must run with NO pass") has always assumed. Progress remains diagnostics-only: later write failures are still best-effort and non-fatal.
- A
github-releaseartifact is capped at its signedsize, so a mis-uploaded or substituted asset can no longer fill the disk the preflight had just protected.atpkg's release download lane passed its own 8 GiB ceiling to curl on both legs (the CDN URL and the API URL) while the caller was already holding the row's signedsizeand passing it as the exact cap on thehttps/pkgvendor lane. The disk preflight (signed size + disk_installedagainst the free-space floor) therefore bounded nothing that lane actually wrote: a row signed at 30 MB whose release asset had been replaced with a 1.9 GB file (GitHub allows up to 2 GiB per asset) downloaded in full intostaging/— through the free floor — and was refused only afterwards by the sha256 gate, which cannot run until the file is complete. The signed size now reaches that lane too, clamped by the ceiling, and a row that states no size (onlyhttps/pkgrows must) keeps the ceiling rather than capping at zero.
- A provisioning run whose
machine.tomlwrite failed no longer reports success and publishes the roster anyway.atpkg-keys setup/joincreated the0600machine key durably, then wrote this machine's public record (id+pubkey+ mint time) with a discarded result through a plainFile::create, and published the master-signed roster regardless. On a full disk — or a crash in that truncate-then-write window — the roster named the machine while its record was missing or zero bytes, and that state was a dead end:tools/atpkg-index.shrefuses to build an index without the record (the index must state WHICH machine signed it, and that id is not derivable from the key) and names one remedy, "re-runatpkg-keys join --id <this-machine-id>" — which the join preflight refuses becausemachine.keynow exists, and whichroster_ops::addrefuses because the id is already on the roster. The operator was left to hand-write the file the script tells them not to hand-edit, or to mint under a new id with the paper master. The record now goes through the same stage-fsync-rename path as every other identity file, and a failure fails the run BEFORE the roster is published, carrying the recovery this module already names for that point (the key exists; remove it and retry). The roster's contents, signature, ordering and lock discipline are unchanged.
- A failed rollback-floor persist no longer leaves its temp file behind.
Floor::writepublishes the anti-rollback high-water mark by writing a sibling<floor>.<pid>.tmpand renaming it over the floor, but none of that sequence's failures — a shortwrite_all, a failedsync_all,set_modeorrename— removed the temp on the way out. The write runs on every accepted pass and its failure is reported rather than fatal (a lost advance never rejects a passed check), so a store volume that keeps refusing it — ENOSPC on the prefix, the exact condition the freespace preflight exists for — accumulated one leftover file per process id beside each of the three floors (floor,floor.gen,roster.floor), and nothing ever removed them: atpkg's other temp sweeps match.tmp-andatpkg-*.tmp, andgchas never heard of this name. A leftover from a failedset_mode/renameholds real bytes, not just an empty inode. The staged write now has a single failure exit that removes the temp before propagating the error. The accept/refuse decision, the monotonic advance, the private-parent refusal, the0600→0644publish and the atomic rename are unchanged.
atpkg verify-indexno longer reports an index-authoring mistake as a signature failure. The verb gave only one refusal —no machine on that roster signed this index— to everything except a badrequiresedge. But the client chain checks Ed25519 FIRST and only then parses the index and binds its attribution, so a malformed or newer-schema index, one carrying nomachine_id, and one whoseroster_seqdoes not match the roster shipped beside it all reach that line with a VALID signature already in hand. A publisher whoseindex.tomlgained a duplicate key from a script edit, or whoseroster_seqwas misread by the indexer'ssed … | head -n1parse, was told their machine had not signed the index — sending them to the paper master to re-join or re-key a machine over a one-line authoring error, whichtools/atpkg-index.shcompounds by promising that a misreadroster_seq"comes back asReject::SeqMismatch", a name the verb never printed. Each post-verify refusal is now named and says the index is the thing to fix. The crypto verdicts are unchanged and stay deliberately opaque (no verification oracle), including the one verdict reachable from both sides — an index whosemachine_idnames a machine other than the key that verified.
- A roster date written with a zone offset is refused instead of being read as UTC. The machine roster's freshness gate parsed
valid_untiland a machine'snot_afterby checking only the separators and a minimum length, so a hand-edited stamp such asnot_after = "2026-12-31T23:59:59+05:30"parsed as if the+05:30were not there. That offset names an instant 5.5 hours EARLIER than the digits ahead of it — the machine's real deadline was 18:29:59Z — so the gate kept it in the live set, and its signatures onindex.tomlandpkg-*.tomlaccepted, for hours past the expiry the operator wrote; the offset range allows as much as 14 h of that skew. The parser now requires the exactYYYY-MM-DDTHH:MM:SSZshape, matching the twin inatpkg's index gate that already refused these strings, so both gates reach the same verdict on the same bytes. Anything else — an offset, fractional seconds, a bare stamp with no zone, or a trailing byte — is unparseable and therefore LAPSED, which is the fail-closed reading the surrounding gate already applied to a typo. No published roster is affected: every producer stamps the…Zform.
- Resolving the signed index no longer verifies the same roster once per candidate. Every release on the index lane publishes the *same* master-signed
aterm-machines.tomlgeneration beside itsindex.toml— on a real store all four cached candidates carry byte-identical roster bytes and signature. Selection's first pass admitted each candidate's roster independently, so three of those four runs re-did the master Ed25519 check, the parse and the admission to rebuild aTrustedRosteralready in hand, on every resolve, in every process. Pass 1 now weighs each distinct(roster bytes, roster signature)pair once and reuses that verdict for the candidates that repeat it. The pairing rule is untouched — a candidate is still admitted only by the roster published beside it, matched on the exact bytes of both halves (never a digest), and the memo lives only for the one call, so a reused generation carries the very clock reading its own admission froze. - A status write that fails no longer strands its temp file in the store.
status.tomlis written through a temp named for the writing process (status.toml.tmp-<pid>) and renamed into place. When the write or the rename failed — a full disk is the ordinary cause — that temp was left in the prefix root, and nothing ever reclaimed it: neitheraterm pkg gcnoraterm pkg doctorlooks for that name, and every pass runs under a new pid, so the leftovers did not even overwrite each other. A machine that stayed full therefore accumulated one stray file per failing pass — roughly one every six hours, plus one per session-spawned pass — for as long as the condition lasted, each of them empty or half-written and none of them ever swept. Both failure paths now remove the temp before returning, the way the progress snapshot and the shim-env sidecar already did.
- A published asset that keeps failing its pin's signed checksum is no longer re-downloaded every six hours. When a build's signed
sha256does not match the bytes the release actually carries — a publish-pipeline slip — the manager refuses the build and deletes the archive it cannot trust, along with any partial that would seed the next attempt from a poisoned prefix. Nothing read that refusal back: the pin was unchanged, so the next six-hourly tick decided to install the same build, downloaded the same asset from byte 0, reached the identical verdict and threw it away again — for the ~3.4 GBtrustbundle that is ~13.6 GB a day, on every machine in the fleet, unattended, until a new index was cut; on a metered connection it is the most expensive thing the manager can do. Both install lanes (a single program, and a coherence tuple applied all-or-nothing) now record that verdict beside the build and skip the download while the pinned build's signed digests are the ones recorded. The first retry is still free — the same verdict comes from a truncated transfer or a bad mirror, and a publisher can repair the asset under the same pin, both of which heal on the very next attempt — so only a second identical verdict starts holding the next attempt off the wire: 12 h, then 24, 48, 96, up to one attempt a week. It is a cooldown, never a verdict: any change to the pinned build's signed digests clears it at once, andaterm pkg install <program>forgets it and retries immediately. Failures that are about the MACHINE (a full disk, an unreadable store, a refused installer lane) record nothing and retry freely, exactly as before, and neither does atree_rootmismatch — its verified archive is kept, so that retry never cost a download in the first place.
- A toolchain kept through one failed install is no longer deleted by the next one. When an install fails *after* activation — writing the
bin/commands hits a read-only or full disk — and the build it was re-staging was already the live one, the manager deliberately keeps the multi-gigabyte verified tree instead of discarding it: the failure is a fact about the machine, not about the toolchain. That keep lasted exactly one attempt. The unwind also removed bothcurrentlinks, and those links are how the *next* attempt recognises an already-live build — so a second run against the same unwritablebin/no longer recognised it and deleted the tree, leaving no toolchain at all on the machine least able to download it again, and leaving the kept tree unwitnessed (and so invisible toaterm pkg gc) in between. The per-programcurrentwitness is now re-pointed at the kept build, which is the state the store was in before the failed run, so the keep holds for every retry.
aterm pkg updatenow revives a tombstoned program, which is what the tombstone told you to do. When a publisher yanks a build, the manager replaces that program's commands with a failing shim that says *"was yanked/revoked — runaterm pkg update"*. Running it did nothing for a program that is not part of a coherence tuple (or one whose whole tuple was tombstoned): the update lane asks which programs are installed by resolving the shims, a tombstone resolves nowhere on purpose, and a group with no installed member was skipped whole — not re-staged, not reported, while the row kept readingtombstonedand the commands kept failing. Once the publisher re-pinned a good build, onlyaterm pkg update <name>recovered it, and on a machine that installed one program by name — never adopting the full toolset,auto_installoff — the set-completion lane that would otherwise have caught it never runs. The pass now also selects a group when one of its members is tombstoned in place: a shim still sitting onPATHrefusing to run, whose name matches an executable of the build the prefix proves live. A shim that is simply *gone* deliberately does not count — that is an interrupted uninstall or a hand-removed file, and this lane must never reinstall a program nobody asked it to. The shim view itself is untouched, which is what keeps the recovery honest: it stays silent for the tombstoned program, so the decision is stillInstall— a re-stage — and neverup to date. Nothing the user removed comes back this way:uninstalldeletes the shims, the store tree and the links, so there is neither tombstone nor witness to find, and a recorded removal or a declined toolset still drops the member before anything is staged.
- A removal
aterm pkg uninstallcould not RECORD is no longer silent. The name of an uninstalled program is written into the prefix'sremovedfile, and that file is the only thing keeping the program out of the unattended lanes: the resumable seed lane and set completion both install "whatever the store lacks", filtered by this record alone, and a launch records adoption again whether or not the user ever asked twice. The write swallowed every error — on a prefix that had become unwritable the command still printedatpkg: uninstalled ayand exited 0 with nothing recorded, and the next launch reinstalled the program the user had just removed, the manager undoing a deliberate act. The write now returns its failure and both callers print it (atpkg: could not record the removal at …: … — the next unattended pass may reinstall ay, and the lifting direction likewise), the way the adoption and decline markers already did.
- An unreadable
status.tomlis no longer REPLACED by the next diagnostic write.status::readreports an absent record and an unreadable one identically (a symlinked path, a hand edit that broke the TOML, non-UTF-8 bytes, an oversize file, a permission denial all read as "none"), and every writer that rebuilds the whole record seeded itself from a default and renamed its single row over the file. One offline update tick was enough: the*index*failure row replaced every other program's state and its signedtree_root(soatpkg verifythen failed closed with "no signed root"), theseamslistaterm pkg uninstall --allwalks to detach the rustuptrusttoolchain (so the link was left dangling), andlast_success_at(so every read-only verb told a machine that had been updating for months that no check had ever run) — silently, with nothing on stderr. The rewriting writers now seed throughstatus::seed_for_rewrite, which distinguishes "no record yet" (create one) from "there is a record and it could not be read" (leave it exactly as it is), and the CLI says so once per process.
aterm pkg uninstall --allno longer reports OS installs and system copies as removed. The sweep walks the union of the live builds and every row instatus.toml, and removing a name the manager never installed is a no-op that succeeds — so a copy the user already had (system: /opt/homebrew/bin/gh — not managed by aterm), an OS-level install obtained through another protocol (installed via pkg: …, the Command Line Tools or Homebrew), a member with no build for this target, an extra nobody opted in to and an agent program still queued were all counted as removals. On a machine whereghcame from Homebrew the command printedatpkg: removed ay, clt, gh, trustand exited 0 whileghand the Command Line Tools were untouched and still ran, and it wiped the very rows that recorded where those copies live. Only a program actually present under the managed prefix is now removed or reported; the copies that stay are named on their own line —atpkg: left alone — not installed by aterm, so nothing was removed: clt (installed via pkg: /Library/Developer/CommandLineTools), gh (system copy: /opt/homebrew/bin/gh)— and keep their rows, which is whatdocs/DESIGN-which-copy-runs-2026-08-27.md§S11 asked for all along ("system copies are never touched and the output says so"). A machine that had nothing of aterm's on it still records the decline and exits 0.
- A pending program the install pass SKIPPED no longer says it was "BUMPED to install next".
__pending— the hidden verb every pending-program stub runs, the thing you read when you type a tool that is not installed yet — folded the terminalskippedphase into itsqueuedarm. So while a pass was running, a member that pass had just held — blocked by an unmet requirement (blocked by <dep>: …), dev-linked, narrowed out of a locked coherence group, held by a local pin, tombstoned, or deferredneeds admin— was answered with "<tool>is queued and now BUMPED to install next — re-run it in a minute", and the honest reason the same pass had just recorded was never printed. Nothing in that pass was going to install it: a skip retires the program from the queue, and the bump file only reorders what is still IN the queue (a clean pass end then deletes it), so the promise repeated every minute until the pass ended. The skipped phase now has its own answer: the recorded reason (control-stripped) withaterm pkg install <tool>as the next act, or — when the skip recorded none — "already current, or nothing in this pass installs it"; neither writes a bump nor claims one.
aterm pkg repairno longer swallows an argument while it rewrites your shell.repairwas dispatched with no arity check — it is the store MUTATOR among the zero-arity verbs — soaterm pkg repair --dry-run, or any other flag or operand, was silently discarded and the verb carried on: it rewrote~/.aterm/shell.dand the rc wiring, re-laid the reroute stubs, re-asserted the rustup seam and re-laid every installed program's shims, printed "repair: shell integration rewritten" and exited 0. The flag was never mentioned, so a user who typed--dry-runread that success as "the preview ran and nothing changed" while their shell wiring had just been rewritten. It now refuses any argument at the usage exit (2), naming the verb, the argument and the arity, exactly asdoctor,statusandgchave since audit D-12.repair --helpstill prints the verb's usage and barerepairis unchanged.
aterm pkg install --default-set --elevate=…no longer accepts an elevation and then drops it. The flag parsed, was carried intocmd_install_elevated, and vanished at its--default-setbranch — which returns into the whole-set bootstrap *above* the one call that sets an elevation policy — so the pass ran at the defaultDeferred:aterm pkg install --default-set --elevate=osascript(the administrator dialog's spelling) exited 0 having elevated nothing, recordedneeds admin — run: aterm pkg install <name>for every OS-installed member that needs one, and never mentioned the flag. The whole-set pass never elevates by design, so the pair is now refused at the usage exit (2), naming the one-program spelling that does take the door. Either half on its own is unchanged.
- An orphaned draft index release is published, not silently “converged”.
gh release createwith assets creates the release as a draft, uploads the assets, and only then publishes it, so a run killed in that window — a shutdown, a SIGTERM at logout, a crash — leaves a DRAFTatpkg-index-(N+1)on the staging registry. Both auto lanes' head discovery filters drafts out, so the next hour re-signed the same number (the index body has no clock input, so the bytes matched), andgh release view/gh release download, which both find a draft by tag, let the indexer converge it and report it *published*. The mirror then made that draft's bytes the public head, and from then on every run of both lanes read a public head one above a staging head that still hid the draft, and died every hour with “another publisher's index is mid-flight” — a message naming no draft. The indexer now readsisDrafton the release it converges: identical bytes are converged and then published (gh release edit --draft=false --prerelease), anything else refuses with the draft named, and a draftness it cannot read is a refusal too. Both lanes' staging probe reads the same flag, so their two refusals say when the release they are looking at is an orphaned draft of their own killed upload.
- A transient
ghfailure is no longer read as “the release is missing”. Both unattended atpkg lanes probed the staging registry with a singlegh release downloadand folded every failure — GitHub’s intermittent HTTP 500 on the release-asset endpoint, a rate limit, a dropped network — into the same empty answer a real 404 gives. One 500 was enough to make the vendor lane forget an authored-but-unpinned build and author that version a second time (a new signed release, the first orphaned), or to kill the run with “the staging registry lost the pinned manifest”; withgh release listfailing in the same hour the derived build number collided with the lane’s own release, and three such hours latched a correctly authored version BLOCKED. The ALab lane demoted an INDEX decision to PACK and re-uploaded over a build that was already staged. Both probes now ask the release-tag endpoint first, treat only a real 404 (or a release carrying no manifest) as an answer, retry the asset fetch, and skip the program for the run when the owner cannot be reached — nothing forgotten, nothing re-authored, no attempt counted.
- A session launch applies the host settings too.
atermrun from another terminal or over ssh could only apply them as a side effect of a package pass that happened to be due — so a Mac with[packages] enabled = false,auto_update = false,ATPKG_DISABLEset, or simply a pass an hour old never got them from that lane at all. The settings take no store lock, need no index and no network, and print nothing when nothing changed, so the lane now runs them on every interactive launch, outside every package gate.
- A pass started from Settings reports what it changed. The packages worker nulled its child's stdout for every verb except
machine apply, so themachine-settings:marker a Check or Install pass printed went nowhere: the card never learned about a change that pass had just made. Both verbs that apply now have their stdout read through the same marker reader the launch lanes use.
- The doctor stops prescribing a remedy that cannot reach anything. Its Spotlight warning ended with
aterm pkg noindex apply --alleven in the branch whose own sentence had just explained that--allskips every directory it had listed — a guaranteed no-op that leaves the warning standing. The remedy is offered only when a pass can reach something, and names the pass's own verb.
- The shared git exclude file can no longer lose an entry. Two writers migrating different targets of the same clone — every linked worktree shares the common
info/exclude, and neither the pass nor a hand-typed apply takes a lock — could interleave a read-modify-write and drop one line, leaving a migrated directory showing as untracked forever. The two lines are appended in one atomic write instead.
HOMEunset no longer discards the[machine]opt-outs. The config is resolved from$HOME, so a process launched without it read no config at all — which for this table means both defaults ACT, silently overriding an explicituniversal_control = "leave"and writing the real account's keys. The config path and the scan root now fall back to the account's home, which is whatdefaultsfollows regardless of$HOME. An emptyHOMEis treated as unset rather than rendering a blank path in the refusal.
- Smaller truths the third audit caught: an unrecognised
universal_controlspelling no longer makes the card quote= "leave"back at a user who wrote something else; the policy warning is printed once per read instead of twice;atpkg machine <word>names the word it refused; a--dry-runthat plans nothing still says "(dry run)"; a pass whose walk hit its budget says so; a failed apply is remembered as a failure rather than as the successful half that preceded it; and the "nothing changed" verdict no longer points at narration the window does not show.
- A build tree that is not cargo's is no longer renamed. The recognizer's weakest signal was "this directory has a
debugand areleasechild", which is also the ordinary two-config CMake layout (cmake -B build/debug) — and on a case-insensitive volume, the macOS default, CMake's and Xcode'sbuild/Debug+build/Releaseanswer the same probe. Beside aCargo.toml, the AUTOMATIC pass renamed such a tree; in a non-git crate it then wrote[build] target-dir = "build.noindex"and aimed cargo into the foreign tree, orphaning the real cache. That arm now also requires a directory cargo itself creates inside the profile (deps,.fingerprint, or the build lock).
- A
target-dirthat points out of the repository is left alone. The rule was "any path whose last component is the old name", so../target,/Volumes/fast/targetand~/builds/target— values that deliberately aim cargo at another disk or a shared cache — were rewritten to the in-repotarget.noindex, silently moving the user's builds back. An absolute value is now compared to the directory being migrated, and a relative one must name it directly.
- A malformed
aterm.tomlno longer changes the machine. Both[machine]defaults ACT, so "malformed ⇒ defaults" meant one typo anywhere in the file — a broken binding three tables away — silently re-enabled what the owner had switched off withuniversal_control = "leave", and said so in a single stderr line nothing surfaces. A file that exists and does not parse is now a refusal: nothing is applied, the CLI and the "This Mac" card both say which file to fix, and the record carriesconfig=unreadable(an appended, optional key, so an older reader is unaffected).
- A
defaultsread that did not happen is no longer reported as the OS default.defaults readof an absent key exits 1 with empty output, which IS the OS default — but adefaultsthat could not run, was killed at its deadline, or answered something unparseable arrived as the sameNone. SoUcPosture::Unknownwas unreachable and a broken read told the user "the cursor roams to other Macs and iPads", with Apply offered, on no evidence. The two are now distinct (KeyRead::AbsentvsKeyRead::Unusable), the posture isUnknown, and both the doctor line and the card say the host could not be read.
- A half-disabled host is not "at the OS default". One key set is a state someone made; the sentence said the default and then, parenthetically, which key was set — contradicting itself in one line. It now reads "half disabled (Disable is set, DisableMagicEdges is not)".
- A Spotlight migration that fails says so. Only the Universal Control half could produce a
machine settings failed —line, so a rename that failed on every pass reached the window as a success with a smaller count. Failures are now their own outcome (Applied::Failed, distinct from a deliberate skip), the pass emits the failed line for them, and the summary counts them separately.
- A launch pass that refused or failed is no longer silent. The launch lanes read a pass's stdout for markers and kept only the change marker, so
machine settings not applied — …andmachine settings failed — …were dropped and the card went on showing the last good record. Both now raise an event, get logged, and land on the card as the last verdict, which also triggers a re-read.
- A scan that did not finish no longer claims the machine is clean. With
$HOMEunset there is nothing to walk, and the read reportedscan=completewith zero exposed directories — telling the reader the machine had been measured and found clean. An absent home is now an incomplete scan, the read says the build output was not looked at, and the "nothing to apply" verdict on both the CLI and the card becomes "nothing to apply from what was seen" whenever the walk did not finish.
- The Settings pager could not reach the two
[machine]switches. The landscape page-scroll limit counted control rows but not the macOS-access and This Mac cards, which the renderer does draw as slices, so on a window that pages the last rows on Security were unreachable. The limit now counts every slice the renderer draws.
- Apply now no longer kills a migration mid-flight for talking too much. The 64 KiB per-stream cap ended the child with a signal, which for
atpkg machine applycould land between a rename and the symlink that keeps the build working. A mutating child is now truncated, not killed, and the pass runs to completion.
- A half-finished migration is repaired by the next pass.
apply_onerolls its rename back on every error it can see, but not on a signal; the leftover state (atarget.noindexwith notargetbeside it) was invisible to every later scan, cargo recreated a realtarget, and that one stayed indexed and refused forever. A pass now re-lays the missing link, in the narrow case only: a git checkout whose pointer is the symlink, with nothing standing where the link goes.
- The
[machine]settings waited for a store lock they never needed. They are applied first thing in the seed/update/install pass BODIES — but the store lock is taken at the dispatch edge, above every body, so a launch whose seed queued behind anotheratpkgholder waited the whole--wait-lockbound (the window passes 30 minutes) before the host settings were touched, and a pass the edge refused outright — a wedged holder withauto_update = false, an unwritable or symlinked prefix — applied them not at all. Nothing in this work touches the store:defaultswrites a per-host preference domain and the Spotlight migration renames directories under$HOME. The edge now applies them BEFORE it reaches for the lock, forinstall,seedandupdate(the quick mutators stay out: a$HOMEscan bolted ontopinis a cost nobody asked for), and the bodies ask through a process-once wrapper that replays what the edge did rather than walking$HOMEtwice.aterm pkg machine applystill calls the raw apply, because an explicit gesture must re-read the machine rather than replay a cached answer. The lock-refusal hint now says the settings were applied above the refusal.
- The automatic Spotlight scan went three levels under
$HOMEand called what it could not see clean. A cargo WORKSPACE keeps its members one level further down than three reaches, so~/<repo>/crates/<member>/targetand~/<repo>/tools/<tool>/targetwere invisible to every automatic pass — whilemachine-state:still saidscan=completeand the CLI, the doctor and the Settings card all said "nothing to apply". Measured on the developer's own machine, which is the case this feature exists for: at depth 3 the scan found 1 exposed target dir (1.1 GiB); at depth 5 it finds 5 (1.5 GiB), and applying now hides the 400 MiB that had been sitting in the index since the feature shipped. Depth 6 found nothing depth 5 could not, on a tree with 29,861 directories in its first six levels.
noindex::DOCTOR_DEPTH is 5, and Budget::DOCTOR rises with it (60,000 directories / 3 s, from 20,000 / 1.5 s) because two more levels is more directories: a full walk of that home measures 1.0–1.5 s, i.e. exactly where the old wall sat, so the scan would have started reporting "at least" on the very tree it was deepened for. The walk stays bounded; the cost of being wrong about the depth is a report that says "at least", not a pass that hangs. the_automatic_depth_reaches_a_workspace_members_target pins the shapes.
aterm pkg machinenow says what it did NOT walk. The count is "under$HOME" but the walk prunesLibrary,Applications,node_modulesand the six folders macOS guards with a consent dialog — an automatic pass may not raise a system modal in aterm's name — so a user whose repositories live in~/Documents/GitHubwas reading a clean bill of health over a place nothing had looked at. The read prints the pruned list and the depth, and namesaterm pkg noindex scan <dir>for anything outside them.
Added
- Cursor-trail sound: multiple spaces are musical. Owner: *"get some kind of musical sound for multiple spaces in addition to the soft word separator"*. After a whitespace run's bass root, every further space plays a short rising bell one lattice degree (v1) / one chord tone (music box) higher per extra space beside the tail's breath — a four-space indent is a small rising figure (C4 E4 G4 C5 on the music box). The steps have their own 75 ms gate (a held spacebar cannot machine-gun them) and advance nothing. The audit made the figure a CYCLE on the music box — the first cut fed the run's space count into a degree it folded by at most sixteen octaves, so a held spacebar's 60th space sat at C8 and its 69th over Nyquist; now the step index is reduced modulo the chord's lit degrees over the root (E4 G4 C5 E5 G5, then over again) and counts only ADMITTED steps — and on the v1 palettes the figure CLAMPS at the 880 Hz roof instead of folding back down.
keyboard_song_abgains aSpace×4probe row,--space-censusand--shift-census(heads/tails and Shifts left silent per rate), a walk-meantingverdict and a dated space-head verdict, and readstrail_sound::MIN_GAP(nowpub) instead of a literal.docs/design/RAINBOW-KITTY-V2.md§34 records the re-rulings of 2026-09-16.
- "Data from other apps" joins the consent roster you can approve in one gesture. macOS guards every other app's own data with
kTCCServiceSystemPolicyAppData— the dialog that reads "would like to access data from other apps" — and a terminal reaches those paths constantly: a script that reads another app's settings, a build that walks another app's cache. It was the one promptable class aterm knew about (it is already a coverage row on the Security panel, andNSAppDataUsageDescriptionis already in the bundle) that the warm-up could not ask for. It is now a fourth member ofaterm_containment::consent::Folder, so *Ask for Folder Access Now* asks for it beside Documents, Desktop and Downloads,aterm ctl privacycarries its row, the Security panel prints its state,tccutil reset SystemPolicyAppDatais in the repair plan, and[privacy] warmup_foldersacceptsapp-data(alsoapp_data,appdata). The default list isFolder::ALL, so an unconfigured Mac gets it.
Its probe path is one first-party container, NAMED rather than discovered: choosing "whichever container is there" would mean listing the container root to pick one, and that listing is itself the guarded access — performed on whatever thread built the list, where a system modal has no business parking. A fixed name is a pure function of $HOME, so the only guarded access in a pass is still the detached warm-up worker's own.
A missing probe path is read as PERMISSION for this item alone: an account that never opened that app does not have its container, and being told the path is absent proves the look was allowed, because tccd answers EPERM before a lookup can report a missing name. The three folders keep the old rule — macOS creates them at account setup, so their absence is a broken home, not a verdict.
*These notes were longer than GitHub accepts in a release body (125,000 characters). The entries above are complete as far as they go; the rest of this release's changelog is in CHANGELOG.md in the source tree at this tag.*
v0.86.02026-09-15
Source release. SHA256SUMS is a per-file manifest of the released tree, with a detached Ed25519 signature (SHA256SUMS.sig) by a machine key named in the attached master-signed roster (aterm-machines.toml + .sig). Verification chain and key inventory: SIGNING.md in the publication engine; the roster pin is committed in aterm crates/aterm-update-core/src/pins.rs.
v0.85.02026-09-14
Source release. SHA256SUMS is a per-file manifest of the released tree, with a detached Ed25519 signature (SHA256SUMS.sig) by a machine key named in the attached master-signed roster (aterm-machines.toml + .sig). Verification chain and key inventory: SIGNING.md in the publication engine; the roster pin is committed in aterm crates/aterm-update-core/src/pins.rs.
ALab · alab.systems · By Andrew Yates · hello@alab.systems · © 2026 ·