= AEGIS: the ARGUS Ergonomics & Graphical Interface Standard
Rudolph Pienaar <rudolph.pienaar@gmail.com>
:doctype: article
:toc: left
:toclevels: 3
:sectnums:
:icons: font

[abstract]
== Abstract

AEGIS — the *ARGUS Ergonomics & Graphical Interface Standard* — is the design language of the ARGUS surface: the visual grammar, the interface philosophy, and the behavioural canon everything on the console obeys. The name is deliberate: an aegis is a protective canon, and everything on the surface is built *under the AEGIS*. Where link:../../../docs/workspace-model.adoc[the workspace model] rules on where logic lives in the mise stack, AEGIS rules on what the surface *is* — what a pane may show, where its machinery hides, what links panes together, and what LCARS means when it is taken seriously. This is a *living document*, updated as the design conversation progresses.

== Status

[cols="1,3"]
|===
| 2026-11-09 (HOST ACCESS: each person's berth, read through porter) | The operator wanted to see, beside each CUBE user on CHRIS, whether they have a berth and what it uses, without a shell on the machine. ITS USERS gains a HOST ACCESS column, read from the ChRIS with the accounts (clause 1: what stands, from its truth, with its age; the section's aside is its read time, READ FROM IT hh:mm UTC): `running · 3% · 210 MiB / 2 GiB` for a berth up (CPU as a percent of one CPU, memory in use over its limit), `stopped · home kept` for a person whose container is gone and whose home volume stays, `none` for a person with neither, and a dash in the column. A porter that starts no berths has answered, so the one quiet line under the listing says `berths are off`; berths that could not be read (an older porter that answers no berth status, a porter that does not answer or refuses the admin, a daemon from before they were read) are said `HOST ACCESS was not read: …` with the reason. The column is last to be shed when the listing narrows, after NAME and GROUPS. It is read over HTTPS through porter, with the admin's credentials the host keeps, so no SSH session is made and nothing in the tenancy changes; porter asks CUBE whether the caller administers it and the berth launcher, the only holder of the container engine, reads the berths. The old promise that host access is the next step is gone: the listing says that starting a berth, stopping it and removing a person's home are not controls here yet, and the column offers nothing (a-control-offers-only-what-can-be-done). Proven by the model's unit tests (the cell's words, bytes in MiB and GiB, a count just short of a GiB shown as 1 GiB), the screen's (the column's cells beside the accounts, the dash and its reason for a porter that cannot say and for an older daemon, `berths are off` for a porter that starts none, the note), and in stratus (the read and each reason) and porter (the route's refusals, the answer, the launcher's usage and kept homes). | `features/vm/model.ts`, `screen.ts`; menu `script.ts`; stratus `chris/live.ts`, `commands/chris.ts`; porter `app.ts`, `cube/auth.ts`, `launcher/launcher.ts`
| 2026-11-09 (the berths are settings, and the band says how many the machine holds) | `chris deploy` takes the limits of one berth (`--berth-cpus`, `--berth-memory`), and CHRIS's settings listing reads them with the berths themselves: BERTHS (on or off; on with a porter, shown off where there is no PORTER), BERTH CPU (empty: 1) and BERTH MEMORY (empty: 2 GiB), each FIELD · SET TO · RUNS as the three before them, edited in place, the row marked when the line differs from what runs. RUNS is read from the ChRIS record `chris status` carries (a record from before limits ran the defaults, and says so as 1 and 2 GiB); where berths are off the limits' rows read a dash, never a number nothing uses. A value the engine would refuse is marked where it is typed and never kept: CPUs positive with at most two decimals, memory positive with at most one, the limits and BERTHS on needing a PORTER, a PORTER not cleared while berths hang on it, BERTHS off while BERTH CPU or BERTH MEMORY is set, and a berth memory the machine cannot hold above the profile's floor, or BERTH CPU above its vCPUs (when its shape is known; the engine refuses the same line). There is no disk row: a berth's disk is not enforced, and a control offers only what can be done. The bands of CHRIS and SPEC say how many berths the machine holds (`HOLDS 3 BERTHS`), the host's memory less the profile's floor over one berth's memory, memory alone deciding (a berth's CPUs are a cap on CPUs it shares with CUBE, not a reservation): the one rule porter's launcher refuses a full spoke by (`@fnndsc/menu`, `berthCapacity_derive`), so the figure the operator reads is the figure the launcher enforces. It is of the limits that run, or the line's before a deploy runs; of the shape the machine stands in on CHRIS, and of the shape the SPEC line declares on SPEC, so a resize shows its effect on the berths before it is built; and nothing is said where berths are off or the shape is not known. Where the line's limits differ from what runs, the CHRIS band adds the count at them (`HOLDS 2 BERTHS · 1 AFTER THE CHANGE`); SPEC does not. Proven by the model's unit tests (the rows, the refusals, the figure), the screen's (the settings listing, both bands) and the shared rule's, in menu and in porter. | `features/vm/model.ts`, `screen.ts`; menu `script.ts`; porter `launcher/policy.ts`, `launcher.ts`; stratus `commands/chris.ts`
| 2026-11-07 (CHRIS reads as listings; a password is set in its row) | The operator walked CHRIS live (argus 0.56.0): COMPUTE, HOST and PORTER were input boxes each with a RUNS line under it, and a password change was batched into APPLY and asked on the strip, away from its row. CHRIS now reads as listings top to bottom: PROFILE, then THE SETTINGS (FIELD · SET TO · RUNS, a value edited in place on a press, one the engine would refuse marked where it is typed and never kept, a row whose line differs from what runs marked as the change a replay makes), the change block, THE SCRIPT, ITS USERS. A password is set in its row: NEW PASSWORD and RETYPE, masked, matched as typed; GENERATE fills both and shows the one it made once; one button, SET PASSWORD · <name>, drawn only when the two can be set; the screen answers the engine's hidden questions itself, so the password never reaches a command line, the log or the strip, and leaves the page as it is sent; no question for an ordinary account (loop law 4a), one in the row for `chris`, whose new password restarts CUBE; the receipt (password set · HH:MM) stands in the row. APPLY carries email and groups only (ledger row vm-user-passwd). When nothing differs from what runs (settings and profile as they run), CHRIS draws no change block: one quiet line on the deploy line says a replay re-checks and what it restarts, with a small REPLAY that still asks before it restarts; the block appears only when a setting or the profile changes. SPEC and SERVICES say "nothing changes" the same way, a quiet line, never a block.
| 2026-11-07 (a control offers only what can be done) | The operator set SPEC's boot disk 150 → 50 GB and data 50 → 150 GB on the store; the screen let the disk move, then refused the whole BUILD over it while its note promised a password and a plan. Law <<a-control-offers-only-what-can-be-done>>: on a machine that stands the disk and data sliders and boxes start at what runs (ONLY GROWS), the OS is fixed, ADD SERVICE offers only kinds and ports that can be added, ADD itself drawn only once the service can be added and what it still needs said in its place (22 is SSH, a port open already, a name not a Linux user, a key that is not public), CHRIS's PROFILE is an ARGUS listing of every profile stratus knows, read from the one list its engine deploys from (NAME · WHAT IT PUTS UP · NEEDS · STATE): MINIMAL available, COMPLETE shown as not built yet with its floor to be set and never selectable, a machine below a floor told why; ITS USERS is an ARGUS listing of CUBE's accounts (NAME · UID · GID · GROUPS · EMAIL · PASSWORD, UID CUBE's own id and GID the same, the user-private group chell's `id` projects, the password never shown), a new user a new row, the groups offered only those CUBE holds (read live, `all_users` always on), every edit of a row a pending change collected into one change block with one APPLY that runs each in turn, the engine asking each in plain words (a password hidden, typed twice or generated and shown once; `chris`'s own restarts CUBE, said on its row); a name or email CUBE would refuse is marked where it is typed, listed with why and left out; what only the engine refuses is listed with its reason and left out while BUILD carries the rest; the note under a button stands only while it can be pressed; a button that cannot be pressed is not drawn at all (BUILD with nothing to build, APPLY with nothing pending, UNDO on a row unchanged, OPEN on SSH with no port).
| 2026-11-07 (a machine's seats are one column; a lever is typed as well as slid; one change block) | The operator walked SPEC live (argus 0.54.0) and asked for one column on every seat: a lever is its label, a slider the width of the pane, a number box in step with it (typed, its range and step said, arrow keys stepping; vCPUs in the shape's own step, memory 1 GiB, disks 10 GB), what runs, and its own share of the cost only when it differs. Under the levers stands one change block, the same on SPEC, SERVICES and CHRIS: a line a change (data volume 50 → 110 GB · grows online · +$2.55; memory 8 → 16 GiB · reboots), the month as it runs against as offered, a BUILD (or the deploy's own act) of ordinary size naming the change or counting them (BUILD · 2 changes · +$14.23 a month), and one short line under it; no big words, no button the width of the pane. Each seat reads top-down: the machine's facts (on SPEC its network, address and OS with what runs), its rows, the change block with its act, then THE SCRIPT, the line as it will run; CHRIS's accounts, their own block, after it. An edited line never says the standing it had before its edit (it read unchanged under ~ EDITED).
| 2026-11-07 (a machine's seats: SPEC, SERVICES, CHRIS, TEARDOWN) | Settled with the operator over the store (docs/dev-bastion.adoc §stand-in for the berths that follow): a machine's seats are SPEC (what SIZE was: the levers, which take the whole width while a run speaks), SERVICES (what the machine opens to the internet, a row each: the web's 80 and 443 with what answers there read live, a custom port saying honestly that nothing stratus put up answers, and public SSH, off unless declared, for one administrative Linux user that is not a ChRIS account, key-only with sudo, drawn with its warning; ADD SERVICE writes on the same `vm new` line as SPEC, and one BUILD names the change and asks), CHRIS (one pane, the SPEC pattern: each field of the deploy line with what runs beside it, the line, then the change as one block, the act in large words, what it means and a button naming it (REPLAY · restarts CUBE and porter; DEPLOY · puts CUBE up; CHANGE · host a → b), asked in plain words with YES · REPLAY or NO · CHANGE NOTHING, the offer hidden while the deploy runs as one run (ASKS, DEPLOY, the outcome large); no DEPLOY WOULD box; then its accounts, each with its host access, said as not built yet until the berths exist; USERS is no seat of its own) and TEARDOWN. The header's EDITED · NOT BUILT chip names the seat whose line differs from what the script stood for (SERVICES EDITED · NOT BUILT on every seat) and is drawn only while one does. The VM LOG is shut to its newest line until its strip is pressed, and opens itself when a run asks or fails. Fixed beside it: declaring the VM again no longer marks its ChRIS changed; a replay of a deploy whose record keeps no versions says what it does (its files written again, Docker restarting what changed in them) where it said perhaps; and the vault keeps the newer deploy, where a session holding an older record overwrote a replay kept from elsewhere (the store read DEPLOYED 2026-11-04 after a replay on the 6th).
| 2026-11-07 (a build's offer is spent once pressed; its question is the act's) | The operator walked batch one live (argus 0.53.0) and found the run still lying at its edges: the offer (the change, declared already, a dashed BUILD, plans, then asks) stood under the run and looked pressable; nothing moved between the password and the first plan while the declaration was written, kept in the tenancy and the stacks readied; a hint line (what each step prints runs in the VM LOG below) read as something to act on; and the question kept the console's form (vm build store: … Continue? [y/N]) under CONTINUE and STOP. Settled with the operator: the offer is spent once pressed, so while a run runs only the run box speaks, and after it the offer returns only if something is still to build; the run box stands the instant the build is confirmed (after the password, when one is asked) with PREPARE under way, saying what it does (writing the script, writing the declaration, keeping it and readying the stacks); the run box carries no hint lines, only what a step said of its work; on the screen the build's question is the act's own, plain (Resize store's boot disk 100 → 150 GB? Grown online, no reboot. $39.95 → $42.08 a month.), answered by buttons that say what each answer does (YES · RESIZE, YES · REBOOT AND RESIZE, YES · BUILD; NO · BUILD NOTHING, NO · MAKE NO MACHINE with what its data keeps billing), while the console keeps its [y/N] line; every other question on the strip is drawn as before. Proven by the controller's and the run's unit tests and walks over `stratus --fake` and the real tenancy (copies of the records, the first second after BUILD captured, NO at the question). | `features/vm/screen.ts`, `run.ts`
| 2026-11-06 (the VM controller: one band, a listing, SIZE in one pane, a build as one run) | The operator walked stratus live, page by page, on the store, and found the VM screens restating one set of facts three ways: a one-machine header drawn over the list of machines (THE MACHINE / MACHINES, NAME [NAME IT ON SIZE], CHRIS DEPLOYED none beside a row serving cube.chrisproject.org), legends describing the screens behind a press (WHAT A ROW OPENS, WHAT CAN BE DONE), a MACHINE seat repeating the header, SIZE split into levers, a cost column restating the spec and an empty output column, BUILD a dashed pill far from what it does, and a build that looked idle (its output only on FOLLOW, a small DONE after the first of three tasks, Terraform's set-up and a literal undefined in the log). Settled with the operator, for the VM controller only (the TD controller and the script screen follow): the header is one band, the same on every seat of a machine (the machine and its condition, the mode, its cost now read from the tenancy, what runs, the lock; EDITED · NOT BUILT only while true), and on the listing the listing's (how many machines, what they all cost, what runs); the machines are an ARGUS listing (NAME, STATE, ADDRESS, SIZE, DATA, CHRIS, COST, READ; a press on a row opens it); the legends and the manual prose go; the MACHINE seat goes for a machine this session builds on, which opens on SIZE (one it can only see keeps MACHINE and TEARDOWN); SIZE is one pane, each lever with what runs, the value offered and its share of the change a month, under them the whole change, what it means, and BUILD naming the change and its cost (BUILD · boot disk 100 → 150 GB · +$2.13 a month); an edited line never wears DONE; a build is one run on SIZE from the press (PLAN DATA, PLAN MACHINE, ASKS, APPLY DATA, APPLY MACHINE, GROW, each waiting, under way, done or not needed, the time so far, the outcome large: BUILT, STOPPED BEFORE THE APPLY · NOTHING CHANGED, FAILED with why); a task running is followed without a press unless the operator stopped following; the VM LOG is the one log; the password that raises a READ session is asked in plain words in a field wide enough to type in. The question a build asks is plain and shown once, on the strip (`vm build store: boot disk 100 → 150 GB. Grown online, no reboot. $39.95 → $42.08 a month. A NO builds nothing; the declaration stays written. Continue? [y/N]`); the log keeps its answer (`vm build store: answered NO`). The levers and the change they make stand side by side under the run, so the change and BUILD stay in view at the desk; the listing folds the index pill every listing leads with, since no line names a machine by its row. The kernel passes on only what a Resource Manager job does (its changes, its count, its errors), never Terraform's set-up, and an empty job log is empty, never the word undefined. Proven by the controller's and the run's unit tests, the kernel's, a walk over `stratus --fake` (list, open, SIZE, disk 100 → 150, BUILD, the run, a NO; a yes, whose grow the fake cannot reach, said FAILED with why), and the same journey over the real tenancy from a private daemon on copies of the records (store's 150 GB declared and not built, said so; BUILD planned both stacks in Resource Manager, the run drawn step by step over three minutes, the question answered NO, nothing applied). | `features/vm/screen.ts`, `run.ts`; `features/script/script.css`, `screen.ts` (OpenAsk exported); stratus `cloud/oci/jobLog.ts`, `builder.ts`, `rmApi.ts`
| 2026-11-05 (a cluster's commit asks before it makes anything) | The audit of the TD controller found COMMIT on a new cluster building at once: the compartment, the records at Red Hat, the discovery ISO with its public link and the plan were made before the first question, which came only at billing and priced compute and memory alone ($0.50 an hour) where SIZE priced the whole ($0.61). Now `cluster build` asks first, before its first step, with everything it makes before it bills and the rate it would bill at, priced whole as SIZE prices it (compute and memory, the boot and ChRIS volumes, the load balancers); a NO makes nothing; billing is asked again with the plan's count at the same rate. The new cluster's COMMIT is BUILD, in the billing tone. A cluster built here is counted in its compartment in the tenancy (`resources`, with `resourcesAt`). KILL arms on its first press and says in place what stopping part way leaves; TEARDOWN's guard says what it stops billing. On the script screen a network's seat draws what stands (`net status` answers a model: the CIDR declared against the one its build made, its stack as the tenancy answers it, its bastion), a new CIDR on a built network says what it would do, and a declaration's change is said as the line's own. Proven by the kernel's tests (the question first, a NO making nothing, the billing price whole, a torn-down cluster refused before asking; the CIDR's consequence and the model), the screens' tests, the smoke stages td-controller, vm-controller and script-screen (15 ok), and a walk over the fake cloud. | `features/controller/screen.ts`; `features/script/screen.ts`; stratus `commands/build.ts`, `cluster/sizing.ts`, `cluster/state.ts`, `commands/cluster.ts`, `commands/net.ts`, `commands/lineStatus.ts`, `net/build.ts`, `net/state.ts`; menu `script.ts`, `substrate.ts`
| 2026-11-05 (CHRIS, USERS, MACHINE and TEARDOWN render what runs) | The second slice under a-change-is-a-loop. The kernel reads what runs of a ChRIS over HTTPS, so a READ session reads it (`chris status`: CUBE's answer and whether the admin signs in, CUBE's accounts from its all_users group every page, porter's `/healthz` with its open sessions and, from now, its version), keeps what a deploy put up (the CUBE image, porter's version), and plans a deploy before it acts (`chris deploy --plan`: the changes against the record, the restarts and why, the porter sessions a restart ends); a replay that restarts or changes anything asks with all of it (twice on 2026-11-04 a replay ended every porter session unasked). The VM controller's CHRIS seat draws THE CHRIS · AS IT RUNS and DEPLOY WOULD beside the line; USERS draws CUBE's accounts beside the script's lines; MACHINE draws RUNS (read from the tenancy, with when) apart from DECLARED (with where it is kept) and the instance; TEARDOWN's guard says what each undoing stops billing and what keeps billing. Proven by the kernel's tests (the reads, every page, a membership's shape; the plan and its question, declined changing nothing), the controller's, and a read-only walk over the real tenancy (CUBE 200 with the admin signed in, porter 200 with no session open, two accounts, the record predating kept versions said as such, TEARDOWN $35.70 a month stopped and $2.13 kept). | `features/script/screen.ts`; `features/vm/screen.ts`; stratus `chris/live.ts`, `chris/plan.ts`, `chris/deploy.ts`, `commands/chris.ts`, `commands/vm.ts`, `backend.ts`; menu `script.ts`; porter `app.ts`
| 2026-11-05 (SIZE is one loop) | The first slice under a-change-is-a-loop, after the walk of 2026-11-04 found no VM loop held. Built on the kernel's truth (docs/stratus.adoc, D16 revised: a declaration kept in the tenancy, what runs read on each listing). On the VM controller SIZE renders what runs beside each lever (RUNS 50 GB, lit when the offer moves off it; the levers' head names when it was read), opens on the levers with BUILD under them, and offers BUILD alone: DECLARE stays on the script screen, where declaring is authoring. BUILD WOULD, at the top of the cost column, says each change against what runs and what it means (a new shape reboots the machine; a disk grows online; a smaller disk is refused), and the cost column prices what runs beside the offer with THE CHANGE a month; once a build here leaves what runs equal to the offer, the note is the receipt, BUILT · WHAT RUNS IS WHAT WAS OFFERED, read back from the tenancy. + NEW offers its levers and its price before anything is sent, and BUILD, its one commit, writes the script, declares and builds. A machine opened from its record, its script not written, wears its seats' states (`play --status --text`). Proven by the controller's unit tests, the smoke stage vm-controller walking the whole loop over the fake cloud (a machine built from + NEW, SIZE rendered, memory 8 → 16 offered, built, read back), and a read-only walk over the real tenancy (store's SIZE: RUNS 50 GB from OCI, the disk offered at 180, +$5.52 a month, BUILD held at READ). Ledger: SIZE's BUILD holds every clause for the sizes and owes clause 1 for ports, backups and OS; + NEW's BUILD is held; SIZE's DECLARE is retired. | `features/script/screen.ts`, `model.ts`, `script.css`; `features/vm/screen.ts`; smoke `vm-controller`
| 2026-11-04 (a declaration and its build are one seat) | The operator moved SIZE's disk slider from 50 to 180 GB on the store, pressed RUN and could not tell whether anything had happened; nothing had (RUN on VM NEW declares and builds nothing), and the COST · AS SIZED price lagged the slider (each move asked `vm cost`, which fetched Oracle's whole price list two to four times). Settled with the operator: BUILD belongs on SIZE as a pill, the separate BUILD seat goes, and the screen says where the operator is looking that a declaration builds nothing. Now a `vm new` or `net new` line holds the build line right after it of the same name, one seat: both lines shown, DECLARE (the declaration alone) and BUILD in the billing tone (the declaration as shown, then the build; raised first when the session must be; a refused declaration builds nothing; the run hook now answers whether a line ended in an error). The seat wears the declaration's state until done, then the build's. A declaration not built wears DECLARED · NOT BUILT in the cost column with what a build would change, as `play --status` says it from the spec the last build made, and that BUILD applies it; the running note stands there too while the build runs. The price is asked a tenth of a second after the last move and an older answer arriving late is dropped; the session reads the price list once and keeps it, the first price saying so and the cost column naming when it was read. The VM controller writes a slider's edit to the script (from git, to the working copy) before the line it changed is sent. Law a-step-is-its-line amended: a step is its lines. Proven by the model's, the script screen's and the VM controller's unit tests and the kernel's (the price list read once, a failed read not kept, the first price's notice, a build line's owed changes, a data-only grow counted built). | `features/script/model.ts`, `screen.ts`; `features/vm/screen.ts`; `app/vmController.ts`, `scriptScreen.ts`; menu `script.ts`; stratus `cloud/oci/driver.ts`, `cloud/driver.ts`, `cloud/fake/driver.ts`, `commands/vm.ts`, `commands/lineStatus.ts`, `vm/build.ts`, `vm/state.ts`
| 2026-11-04 (one truth for a VM, on every host) | The operator found the store read-only from the porter session on the host it was built on, as the same user: its records and SSH key lived in the terminal stratus's folder, not the berth's. Settled with the operator: the tenancy is the single truth, a host's folder only its cache (docs/stratus.adoc, D16), and resizing a VM that stands is in scope, a reboot said before it happens. The pane follows the kernel: a VM another host built reads as built, whole, with every seat (MACHINE, SIZE, BUILD, CHRIS, USERS, TEARDOWN), its KEPT fact saying it was read from the tenancy; only a VM the tenancy keeps no SSH key for (`keyless` on its row: built before keys were kept, `vm keep` on the host that holds it) or one made outside stratus stands with MACHINE and TEARDOWN, chipped NO SSH KEY IN THE TENANCY or MADE OUTSIDE STRATUS. SIZE says it resizes (vCPUs, memory, disks, ports); BUILD's question carries the kernel's REBOOTS warning and the filesystems' growth. Proven by the controller's unit tests (a machine read whole, a keyless one) and the kernel's (a second host reads the store whole and resizes it; the first host's cache follows). | `features/vm/screen.ts`, `model.ts`
| 2026-11-04 (the VM pane shows what is there, and adds) | The operator: entering the VM pane should show the machines there and let one be added or removed, as a folder shows its trees and makes a new one. The journey, agreed before building: `vm` (and the tile's door) lands on MACHINES, a row per VM the tenancy runs (what it is doing, address, size, what runs on it) with OPEN and + NEW MACHINE; a machine opens on MACHINE, its facts, then its lines; a VM this session can only see (found, or taken over) stands with MACHINE and TEARDOWN alone and no act writes a script for it; acting on a found VM takes its records over from the cloud on the first act, born protected (the operator: nobody adopts a folder before deleting it). The VM's TEARDOWN seat draws the kernel's steps as blocks (the machine, then the data or its keeping: STANDS, GOING, GONE with the time, STAYS), moved by the VM state event the teardown sends after each step (`vmController.event_take`); a found VM shows the ChRIS its instance's tag names (`chris deploy` tags the instance); SIZE no longer calls a script read from a machine's record edited. Proven by the controller's unit tests and two walks with screenshots: over the real tenancy from a session with no records (the store found, its lock lifted, taken over; nothing torn down) and over the fake cloud (a machine built, every seat, its teardown run, the blocks caught going). | `features/vm/screen.ts`, `model.ts`; `app/vmController.ts`, `main.ts`; `features/controller/controller.css`
| 2026-11-03 (the VIRTUAL MACHINES tile lists what the tenancy runs) | The operator signed in through porter and the tile read NONE while the store VM served cube.chrisproject.org: porter's session keeps its own records, and `vm status` read only those. The kernel now reads the tenancy (docs/stratus.adoc, D16 note of 2026-11-03), and the tile lists every VM it runs: a kept one as before, one this session keeps no record of as `FOUND · RUNNING` (the cloud's word) with its address; the VM controller's condition reads the same, and a found VM's synthesized script takes its size from the cloud. Proven by the tile's unit test and the dashboard over a session with no records on the real tenancy (the store listed, 1 VM). | `compositions/stratus/tiles.ts`; `features/vm/model.ts`; `features/launcher/about.ts`
| 2026-11-03 (a takeover's gutter and head go away on the press, as the page's do) | The operator: pressing the same element in the frame hides it in ChRIS; not in stratus; the OCI block pressed twice should roll the top frame away. Observed over `stratus --fake`: the page's dismissals work in a stratus session (DASHBOARD or FILES pressed again, the header face pressed again); what lacked them was the takeover frame, whose gutter of views and whose head stayed fixed under the TD controller, the VM controller and the script screen. Now `takeoverFrame_build` sends the gutter off stage on a press on the view already lit (`data-gutter='away'` on the takeover root, the page's own translate and negative margin, the head's elbow staying) and rolls the head off the top on a press on the brand block (`data-header='away'`, the head out of the flow and glided up by its own height, the stage flowing up, as the page's header leaves; the takeover's head has one face, so one press departs where the page's panel selects first), a press that navigates brings the gutter back first, a strip at each edge (the page's `#gutter-restore` and `#header-restore` on the frame) or Esc brings them back, and the three hosts' Esc restores gutter, then head, then leaves, one layer at a time as the page's Esc does. Law: a-takeover-keeps-the-frame, applied to the frame's dismissals. Proven by the frame's unit tests and drives over `stratus --fake` on the controller. | `features/takeover/frame.ts`; `lcars/takeover.css`; `features/controller/screen.ts`, `features/script/screen.ts`; `app/tdController.ts`, `vmController.ts`, `scriptScreen.ts`
| 2026-11-03 (a session has its own theme: sickbay for ChRIS, the lower decks for the cloud) | The operator, again: can the theme be enforced in code, ARGUS in sickbay blue, stratus in LOWER DECKS? It was one choice per browser (`argus-theme`), painted at page boot before the backend had answered, so a cloud session wore whatever the ChRIS session last chose. Now the theme pill is told the session once `backend_ask` answers: a ChRIS session reads and keeps `argus-theme` (MEDICAL when nothing was chosen), a stratus session `argus-theme-stratus` (LOWER DECKS), each repainted on the root's one declaration, the pill still cycling within the session. Proven on `stratus --fake` with a fresh profile: boots LOWER DECKS with `composition` stratus, a press remembered under the stratus key alone, a ChRIS choice planted beside it ignored across a reload; the ChRIS session reads the key it always did. | `app/main.ts`
| 2026-11-03 (a teardown is steps, each a block, red until it goes, gold once gone) | The operator, after the first fix: the teardown should show as LCARS blocks, each step going from red to green. The kernel kept no step of a teardown (one job, seven removals inside it, `gone` at the end), so nothing could be drawn and a teardown stopped part way left a record that read SERVING. Now the kernel keeps each removal as a step on the record (destroy, dns, image, volumes, stack, compartment, installer, in the order they run; `cluster <name>` lists them; the model's `teardown`), shows the state as each begins and ends, and resumes at the first step not done; a stopped teardown reads PARTLY TORN. The TEARDOWN seat draws the steps as the BUILD strip's blocks (`tdc-strip-step`, the same markup, the teardown's tones): STANDS in red, GOING lit, GONE in gold with the time, NOTHING when there was nothing of it, FAILED with why; the guard says STOPPED PART WAY and where TEAR DOWN resumes; the OPENSHIFT tile's row says PARTLY TORN. The palette has no green: done is the build's gold here as on the strip. Proven by the kernel's tests (a pass that fails at the records, the next resuming past the destroy), the screen's, and a drive over `stratus --fake` with screenshots: under way, KILL, PARTLY TORN, resumed, GONE. | `features/controller/model.ts`, `screen.ts`, `controller.css`; `compositions/stratus/tiles.ts`; stratus `cloud/builder.ts`, `cloud/oci/builder.ts`, `cloud/fake/builder.ts`, `cluster/teardown.ts`, `state.ts`, `readout.ts`, `commands/teardown.ts`, `cluster.ts`; menu `substrate.ts`
| 2026-11-03 (a teardown is seen running, and seen to end) | The operator tore harbor down from the controller (the password, the name typed back) and saw nothing for the quarter hour the destroy took: no progress, no end. Three causes, read on the daemon's own files and the tenancy. The kernel's paged read of the Resource Manager log fails (`invalid pagination token`) a few thousand lines in, on every poll, so no line of the destroy was passed on; the cluster's state looked only for the build's task, so a teardown was never `running` and the condition never moved; and the controller's RUNNING box, clock, KILL and THE STEP'S LOG stood on BUILD alone, the step log fed from build and plan lines only. Now the kernel reads the log as one text, carries the teardown's task (`/proc/steps/<name>-teardown`, step `teardown`) with the condition TEARING DOWN, and `cluster teardown` shows the state as the task starts and as it ends; the TEARDOWN seat wears RUNNING · TEARDOWN with SINCE moving, KILL and FOLLOW (`wait <name>-teardown`), THE STEP'S LOG as the lines arrive, then TORN DOWN with when, the log `<name>: torn down; nothing of it bills` with a cue, and the cluster stays on its seat as GONE after the read drops it (torn down and gone is history to the kernel, not a cluster). The OPENSHIFT tile's row says TEARING DOWN. The law a-running-step-is-followed-not-held, applied to the teardown. Proven by the readout's and the screen's unit tests, a drive over `stratus --fake` with screenshots, and a plan job on the real tenancy whose log streamed while it ran. | `features/controller/screen.ts`, `model.ts`, `controller.css`; `compositions/stratus/tiles.ts`; stratus `cloud/oci/rmApi.ts`, `cluster/readout.ts`, `steps.ts`, `commands/teardown.ts`, `build.ts`; menu `substrate.ts`
| 2026-11-02 (no floating ask: the password in the frame) | The operator: a floating window violates LCARS. The MANAGE password sheet (`app/cloudDoor.ts`, the share-sheet markup reused) is deleted. `elevate_ask` puts the question where the control was pressed: on the TD controller's, the VM controller's or the script screen's strip (the engine's own form, a hidden field, STOP keeps READ), or on the console's input line when no takeover is up (masked, never in the transcript). The Escape guards for the sheet go with it. Law: a-question-stands-in-the-frame. | `app/main.ts`, `tdController.ts`, `vmController.ts`, `scriptScreen.ts`; `console/argusLang.ts`; `lcars/argus.css`
| 2026-11-02 (the read after a line is said; Escape is the sheet's) | The operator, on the real tenancy: LIFT THE LOCK seemed to do nothing, TEARDOWN never came. Reproduced on a private daemon over copies of the records: the line ran, but the host reads `cluster status` again before the seat changes (a tenancy search, 6 to 12 s), with the pill disabled and nothing said; and Escape over the password sheet closed the controller behind it, stranding the sheet. Now the controller's `run` and `refresh` are two hooks: after the line it logs the read, shows READING under SINCE LAST COMMAND and on the TEARDOWN seat until the clusters arrive. The three takeovers leave Escape to the sheet while it is up, and the sheet takes Escape at the window. A held control on a daemon with no door says so, not "press again". | `features/controller/screen.ts`, `controller.css`; `app/tdController.ts`, `vmController.ts`, `scriptScreen.ts`, `cloudDoor.ts`; `features/takeover/tier.ts`
| 2026-11-02 (the VM controller: a screen keyed by the machine, the script as what it leaves) | docs/stratus.adoc D17 revised with the operator, who found no way into VM management: the script screen was the only door, the VM an inside of it. Now `vm` or the VIRTUAL MACHINES tile opens the VM controller (`features/vm/screen.ts`, the script screen keyed by one machine): a pill per machine and + NEW on the gutter, the machine's lines as seats SIZE, BUILD, CHRIS, USERS, and TEARDOWN as the machine's own undoing; the header the machine's facts (`vm status`), its condition DECLARED, DATA ONLY, BUILT, GONE or RUNNING, the MODE pill beside it, the lock chip. A new machine is named and committed: its script is written under `/scripts/mine/` with its declaration and its build, then `vm new` is sent; a machine the host keeps with no script gets its script from its record, written at the first act; a CHRIS not in the script is appended by RUN before it runs. The script screen's bodies are the controller's (the class opens its members to it); `vm status`, `new`, `build` and the rest stay the session's. Proven by the model's and the controller's unit tests, the tiles', the smoke stage `vm-controller` over `stratus --fake` (a machine named, committed, declared, standing in the gutter), and screenshots. Law: a-controller-leaves-a-script. | `features/vm/model.ts`, `screen.ts`; `features/script/screen.ts`, `model.ts`, `script.css`; `app/vmController.ts`, `main.ts`; `compositions/stratus/lang.ts`, `tiles.ts`; `features/launcher/about.ts`
| 2026-11-02 (MODE on every stratus screen; a READ session is raised by password) | The operator, in a READ session, pressed TEAR DOWN on the controller and read `tearing down needs the tasks tier`: the kernel's name for a choice made at the door, which no screen showed and no press could change; and READ and MANAGE at the door read like two users. docs/stratus.adoc D14 revised with them: every session enters as READ, nothing asked; the first control whose line would act (a build, an adoption, a teardown, a deploy, a user added, RUN on such a line, ADD) asks the person's password once on a sheet (`app/cloudDoor.ts`, `password_ask`), the daemon checks it at the CUBE door porter named to it (`elevate` on the wire), and the session is MANAGE until the page leaves. Both takeovers wear a MODE pill beside CONDITION, `READ` or `MANAGE`, and the dashboard's strip a `MODE READ` readout; a control that asks is drawn dashed with the ask in its title; where the daemon has no door (chell's own) it reads `· REFUSED` with the reason; a more particular reason (adopted, protected, found) wins. The kernel's refusal says `entered as READ, so nothing outside this host changes; MANAGE asks your password once`; `cluster status` carries that hold on BUILD, ADOPT and TEAR DOWN. `stratus oci` enters READ without a question; `stratus oci manage` asks the password in the session. The halted condition is PLANNED (before anything bills) or PARTLY BUILT, never STOPPED, since nothing had run that could stop. VIRTUAL MACHINES follows FILES: `vm status` (now a model) with the ChRIS on each or its size and what it bills, a row opening the script that declares the VM. Found the same evening by the operator on pangea: the kernel's `cluster status` carried the mode as a refusal, so TEAR DOWN read REFUSED in READ instead of asking; it carries the cluster's own reasons only now, and the guard says the ask. And every cluster ever torn down stood as a pill in the header: one with nothing of it left in the tenancy is history now, named in `cluster status`'s last line and listed nowhere (D15). Proven by both screens' unit tests, the tiles', the language's, stratus's, calypso's (`elevate`) and porter's. | `features/takeover/tier.ts`; `features/controller/screen.ts`, `model.ts`, `controller.css`; `features/script/screen.ts`; `app/cloudDoor.ts`, `hostContext.ts`, `main.ts`, `status.ts`, `tdController.ts`, `scriptScreen.ts`, `consoleHost.ts`; `console/argusLang.ts`; `compositions/stratus/tiles.ts`, `chris/tiles.ts`; `features/launcher/about.ts`; menu `messages.ts`, `script.ts`, `substrate.ts`; calypso `daemon/elevation.ts`, `server.ts`, `launch.ts`; porter `app.ts`, `host/processHost.ts`; stratus `tier.ts`, `cluster/readout.ts`, `commands/vm.ts`
| 2026-11-02 (a running step is followed, never held; a hidden word is an errand) | docs/stratus.adoc D17's remainder, the same day. A step that runs long is a task of the session under `/proc/steps/`, and `play --status` now names it beside the line's state (`running`), so the screen never holds a build: it follows the task with the session's own `wait`, the log under the step, the seat wearing ▶ and the header RUNNING with STOP FOLLOWING, which is the console's Ctrl-C and frees the lane while the task goes on; LEAVE does the same; a script opens on its running seat and follows it again (FOLLOW, when the operator had stopped), and the SCRIPTS tile reads RUNNING · VM BUILD meanwhile. A line that starts a task re-reads the standing a moment after RUN, so the seat runs while the line still holds the lane. A hidden word (a user's password, the admin's) is asked on the errand's own bar across the top of the body (`errand-bar`, the caption, one hidden field, SEND and STOP), its field emptied as it is read; the strip keeps the words that are seen. Proven by the model's, the screen's, the tiles', stratus's and brasa's unit tests. Law: a-running-step-is-followed-not-held. | `features/script/model.ts`, `screen.ts`, `script.css`; `app/scriptScreen.ts`; `compositions/stratus/tiles.ts`; menu `script.ts`; brasa `builtins/res/play.ts`, `core/backend.ts`; stratus `commands/lineStatus.ts`
| 2026-11-02 (the script screen: a takeover over a `.mise` script) | docs/stratus.adoc D17, grilled with the operator 2026-11-02 and built the same day. In a stratus session `scripts` takes the display over the library (`ls /scripts`: each source's scripts with where their lines stand), and `scripts open <path>`, OPEN on the library or a row on the new SCRIPTS tile, over one script: its lines become the rail, one seat each (NET NEW, NET BUILD, VM NEW, VM BUILD, CHRIS DEPLOY; every `chris user add` line gathered under USERS), each wearing the state `play --status` read for it from the host's records, not yet, ready, done or changed, never a visit. A step's body is a form over its line (the CIDR; the sliders scaled by `vm limits`, the data backup, the ports, the OS; the profile, compute, host, porter and password file), the line shown as it will run, RUN sending it as the operator could have typed it, the console's output under it as the proof; VM NEW re-asks `vm cost` as its levers move and the header shows what the declared VMs bill now and what the step would add; VM BUILD carries PROTECT ON and OFF and TEARDOWN, the machine or `--all` on a redder pill; USERS lists the user lines and ADD appends the next, its password asked hidden when the line runs unless a file is named. A form's edit is held as EDITED · NOT SAVED until SAVE writes the whole script to `/scripts/mine/<name>.mise` with the editor's own `touch --withContents` line and opens it from there; a source's clone is never written. The engine's questions stand on the strip, CONTINUE and STOP for a `[y/N]`, a hidden word typed unseen, a name typed back for `--all`. The header: the script's standing (its counts), its name, source and path, the cost, the line running and for how long. A prompt at the foot takes any line. LEAVE or Esc gives the page back. The frame (`features/takeover/frame.ts`) now lets a screen replace its views (`views_set`), since a script's seats come from data. Proven by the model's and the screen's unit tests, the tiles' test, the smoke stage `script-screen` over `stratus --fake`, and live against the store on pangea (`store.mise` from HARBOR-Medical-AI/infra: four lines done, USERS ready). Law: a-step-is-its-line. | `features/script/model.ts`, `screen.ts`, `script.css`; `app/scriptScreen.ts`, `main.ts`; `compositions/stratus/lang.ts`, `tiles.ts`; `features/launcher/about.ts`; `features/takeover/frame.ts`; menu `script.ts`; stratus `commands/vm.ts`; brasa `builtins/res/play.ts`
| 2026-10-31 (DOMAINS is OPENSHIFT; the controller answers `cluster`) | docs/stratus.adoc D16, grilled with the operator: a foundation is built first and an application built into it, so OpenShift, a VM and ChRIS are layers and a Trusted Domain is the recipe of OpenShift with ChRIS on it, not a machine kind. The bare cluster's verbs are `cluster` (`cluster new`, `cluster build`, `cluster status`…), and the stratus dashboard's first tile is OPENSHIFT, counting CLUSTERS where it counted TRUSTED DOMAINS. `cluster` and `cluster <name>` take the screen as the controller, as `domain` did; the controller's pills, empty view and log speak of clusters; the prompt's segment key is `cluster`. The Trusted Domain mark and splash stay: they brand the cloud session, whose purpose is Trusted Domains. Folder `clusters/` under the tenancy, the tag `mise-cluster`.
| 2026-10-31 (a document reads as its page) | The operator: an opened `.md` or `.adoc` showed as plain bytes everywhere, the Viewer pane included, and AsciiDoc had no colours even in the editor. Now a file opened to read in the browser shows its source in the editor's colours (any language the editor knows, past 512 KB plain), and a Markdown or AsciiDoc document carries RENDERED on its header for the page it describes, SOURCE to return; a Viewer pane beside the browser shows such a document rendered. AsciiDoc gains a line-oriented syntax mode, shared by the editor and the read view (`features/edit/syntax.ts` holds the one colour table both use). The engines (marked, Asciidoctor in SECURE mode, DOMPurify over both) load only at the first render. A rendered document stays inside its pane: no script or style, its anchors renamed so none meets the page's ids, an in-document link scrolls the pane, a link off the page opens a new tab, a link to a neighbouring file goes nowhere, an image is named by its alt text rather than fetched. Proven by unit tests of the renderer and the colours, and smoke `files-document` (both kinds, both views, the Viewer pane). Law standing: a-file-reads-as-what-it-is. | `features/view/document.ts`, `documentEngines.ts`, `panel.ts`; `features/edit/syntax.ts`, `asciidoc.ts`, `field.ts`; `features/files/panel.ts`; `lcars/argus.css`
| 2026-10-29 (the cloud door asks READ or MANAGE) | docs/stratus.adoc D10 and D14, argued with the operator: whether a stratus session may act on the tenancy is fixed when porter starts it, and it was asked for with `stratus oci --spend`, a flag no control on the surface could reach and a word nobody would know to type, which broke the rule that what the console does the dashboard can do. Now bare `stratus oci` asks at the door, as trek asks its settings before the first turn: a sheet with READ (look only; nothing outside this host changes), MANAGE (act as well, each act after its own lock and the name typed back) and NOT NOW, answered by a press, the answer's first letter, or Escape. `stratus oci read` and `stratus oci manage` answer in words; `--spend` and `--read` are refused by name. The ChRIS dashboard gains a CLOUD tile where the page came through a porter, after the UNIVERSE; it runs the console's own line, so the question is one question pressed or typed. The tier itself stays fixed for the session: changing the answer is entering again. Proven by unit tests of the verb, the sheet and the tile. | `app/cloudDoor.ts`; `console/argusLang.ts`; `compositions/chris/tiles.ts`; `features/launcher/about.ts`; `app/main.ts`
| 2026-10-26 (CAPACITY opens a pane; SIZE asks on open) | #1284, found live on pangea by the operator: pressing the CAPACITY tile, or a row on it, sent `capacity` as a console line and the answer printed as text, so the press read as dead; and the controller's SIZE sat on "reading what a domain is sized against" because NEW opened on SIZE without asking for its sizing. The tile and its rows now open the CAPACITY pane (`features/capacity/panel.ts`, `app/capacityPane.ts`): the `stratus.capacity` model as a listing under the façade, one block per family, a row per limit and zone, FAMILY · LIMIT · ZONE (the expanse) · USED · FREE · OF, the figures red where nothing is left, the bar naming the region and the families; the typed `capacity pane [<family>]` opens the same, and bare `capacity` stays the session's text. The tile's rows read NOT READ, not NO FAMILY, when the session did not answer, since `capacity` now falls back to every family the region offers when no spec names one. `domain_choose` on NEW asks `domain size` as a press on SIZE does. Proven by the pane's rows test, the tile test, the controller screen test and stratus's capacity test. | `features/capacity/panel.ts`; `app/capacityPane.ts`, `main.ts`, `panes.ts`; `console/argusLang.ts`; `compositions/stratus/tiles.ts`; `features/controller/screen.ts`; `lcars/argus.css`, `index.html`; `packages/stratus/src/commands/capacity.ts`
| 2026-10-26 (DOMAINS lists the tenancy) | #1286, docs/stratus.adoc D15. The operator's first stratus session on pangea's porter read DOMAINS · NONE while the tenancy held harbor, serving and billing: the session listed only its own folder. `domain status` now lists every domain the tenancy holds by its `mise-domain` tag, so the tile and the controller show them; one this host keeps nothing of wears the class FOUND · NOT ADOPTED, its row reads so in place of a last step, its OVERVIEW note says what it is and carries ADOPT, which sends `domain adopt <name>`; PLAN, VERIFY, BUILD and TEAR DOWN read REFUSED on it with "adopt <name> first". Proven by the model's badge test, the tile's row test, and stratus's own tests over the fake cloud. | `features/controller/model.ts`, `screen.ts`; `compositions/stratus/tiles.ts`; `packages/stratus/src/commands/{domain,build}.ts`, `cloud/driver.ts`
| 2026-10-27 (a paint does not wait on the region) | Found live on pangea by the operator in a stratus session: the console attached, printed its banner and offered no prompt for minutes, and the dashboard stayed empty. The dashboard asks `capacity` at paint, which with no family named walks every family the region offers, limit by limit and zone by zone, one read at a time against a throttled API; stratus declared no holds, so that read held the whole session and the console's greeting (`motd`) waited behind it in the lane. Now `capacity` and `spend` hold nothing and run beside any line; `capacity` keeps what it read for ten minutes and reads four limits at once; and a paint waits one and a half seconds for `capacity`, paints DOMAINS with CAPACITY · READING when it has not answered, and is painted again when it lands, one `capacity` on its way at a time. Proven by the tile test (a slow capacity, one ask, the repaint), stratus's capacity test (the second read asks the cloud nothing) and the backend test (the holds). | `compositions/stratus/tiles.ts`; `app/main.ts`; `packages/stratus/src/commands/{capacity,domain}.ts`, `backend.ts`
| 2026-10-26 (a takeover keeps the frame) | #1285, found live on pangea by the operator in a stratus session: the TD controller's gutter was a hundred pixels wider than ARGUS's, its head and body elbows met flush with no gap, its body opened on bare text under no strip, and its readouts wore trek's column heads; trek's bridge, which the controller was built from, had the same geometry. Both drew a frame of their own. Now one frame serves every screen that takes over: `features/takeover/frame.ts` builds the page's own skeleton — the gutter at `--lfw` with a brand block and a seat at its head, the banner, a face and the band beside them, the gap, the views as the page's seats (`panel-3` on, LEAVE closing the column as `panel-7`, the chosen one lit as LANG is when live), and beside them the body's elbow as the page draws it with the console closed: the band at the head of the column, flush with the first seat, its inner fillet under the band's left end, both in the colour of the seat they join (lit with it when that view is chosen), and the page's own `main` below holding the body and the log as panes, each under its titled strip. Its measurements are argus.css's: each `takeover-*` class is listed beside the page's own (`.left-frame-top`, `.right-frame-top`, `.wrap`, `.left-frame`, `.right-frame`), so a change to the frame moves every screen drawn in it. The bridge and the controller keep only their content. Proven by the two screens' unit tests, the smoke stages `trek-bridge` and `td-controller`, and the theme canon (zero differences on the page). Law: a-takeover-keeps-the-frame. | `features/takeover/frame.ts`, `lcars/takeover.css`, `lcars/argus.css`; `features/controller/screen.ts`, `controller.css`; `features/trek/bridge.ts`, `trek.css`
| 2026-10-25 (a pull runs as a joblet; STOP is kill %N) | Epic #1273, slice 5, grilled with the operator (question 9: "this means that long running tasks don't hog the queue?"). PACS PULL (a study, a series, PULL N SHOWN) and the cohort's pulls (all, a row) send `pull … &`: the line the operator could type, echoed as it is sent, run as a joblet, so the console and every other press stay the operator's while a retrieve goes on. The badges follow it as before: a joblet relays its progress to the surface it came from, and hands that surface any model it answered with (the cohort its pull brought home), which the ambient routes already take. Each running joblet is a row on the LANE instrument, `%1 pull … · RUNNING` (or `WAITING · FG %1 TO ANSWER`, amber), with a STOP that types `kill %1` at the console; the console prints `[1] … &` when one starts and `[1]+ Done`, `Failed (exit N): …`, `Stopped` or the waiting line when it ends or asks. The RESTART question names each joblet it would end (its log kept), and a porter login that finds the daemon on older code keeps it, rather than restart it, while joblets run. Presses whose next step reads the answer, or that ask a question in front, stay in front: CREATE FEED and PROCESS (the cohort adopts the feed they answer), catalogue RUN (it reads the scheduled feed), feed DELETE (it confirms), DAG REFRESH (index movement, already off the lane), and the TD controller's build and teardown (they follow their steps aside already, and the screen is built on following them). Proved live on a private daemon: `du ~ &` ran as `%1` with its LANE row and STOP, `nosuchcommand &` printed `[2]+ Failed (exit 1): chell: command not found: nosuchcommand`, and STOP printed `[1]+ Stopped  du ~` and took the row away. | `app/laneInstrument.ts`, `app/restart.ts`, `app/main.ts`, `features/pacs/panel.ts`, `features/gather/panel.ts`, `lcars/argus.css`, brasa `core/joblets.ts`, calypso `daemon/server.ts` (`/joblets`), porter `versions.ts`, `app.ts`, menu `messages.ts`
| 2026-10-23 (the DOMAINS tile is the controller's door) | Epic #1056, docs/stratus.adoc D13. Pressing the DOMAINS tile opened home (the files listing of domains), so on a tablet the controller could only be reached by typing `domain`. The tile now sends `domain`, opening the TD controller on the domain it chooses, with the others as pills; its verb reads OPEN THE CONTROLLER, and a row still opens its own domain. Home stays on FILES. Alongside it, an adopted domain shows the ChRIS steps its checkout did: harbor read 11 of 15 and UP because adoption carried only the machine's steps; adoption now also carries `postinstall` and, from the checkout's evidence, `chris-deploy`, `chris-seed` and `smoke`, and `domain adopt` on an adopted domain catches up (harbor now reads 15 of 15, SERVING). Proven by the tile and adoption unit tests and live on harbor. | `compositions/stratus/tiles.ts`; `features/launcher/about.ts`; `packages/stratus/src/domain/adopt.ts`, `commands/build.ts`
| 2026-10-23 (the tiles answer while a build is followed) | Epic #1056, docs/calypso.adoc (the session daemon). Live, quay's build was followed by a play for an hour, and a second ARGUS opened on the session drew an empty DASHBOARD: every tile's read waited on the lane behind the follow. A follow now steps out of the lane between the task's lines (`aside`, as a question already did), so tiles, the controller's reads and other lines run meanwhile. The same session showed two more faults, fixed with it: `domain quay` typed in a browser whose tiles had not claimed the name opened the controller in another browser (a browser's own line is now its own or the session's), and the play left behind by a closed browser could not be stopped from the one still open (a line whose surface has gone is anyone's to stop). Proven by calypso's stop and surface-line tests, brasa's runner and turn tests, and live on quay. | `packages/menu/src/surface.ts`; `packages/brasa/src/core/surface.ts`, `runner.ts`, `turn.ts`, `engine.ts`; `packages/calypso/src/daemon/server.ts`, `launch.ts`; `packages/stratus/src/commands/build.ts`
| 2026-10-23 (BUILD builds as far as the steps go, ChRIS included) | Epic #1056, docs/stratus.adoc D14. The build's steps now run through ChRIS (`postinstall`, `verify`, `chris-deploy`, `chris-seed`, `smoke`), so BUILD sends `domain build <name>` with no `--through`, and `tenant.mise` claims the smoke step done. The grid and the CHRIS view show those steps as any other, with their evidence; a domain whose smoke passed reads SERVING. SMOKE AGAIN reads SMOKE AGAIN · LATER: the build runs the journey once, and running it again is a line not written yet. Proven by the model and screen unit tests and a fake build through `smoke`. | `features/controller/model.ts`, `screen.ts`; `packages/stratus/manifests/tenant.mise`
| 2026-10-23 (a build left part way reads STOPPED) | Epic #1056, docs/stratus.adoc D13. A domain with steps done, none running and not up was SIZING · NOTHING BUILT, as if nothing had run: quay answered NO at the billing question with five steps done and read so. It now reads STOPPED, drawn in SIZING's honey; the line under it says PLANNED · NOTHING BILLED when the next step is the first that bills, NOT FINISHED otherwise (BILLING · NOT FINISHED once money runs). SIZING means a spec and nothing run. Proven by unit tests for `condition_derive` and `conditionWhy_derive` and the fake build's last state event. | `packages/menu/src/substrate.ts`; `packages/stratus/src/domain/readout.ts`; `features/controller/model.ts`, `controller.css`
| 2026-10-23 (SIZE chooses the zone a new domain's names live under) | Epic #1056, docs/stratus.adoc D14. The `stratus.domain.size` model gains `dnsZones`, the tenancy's DNS zones, and its `baseDomain` falls back to the only one. SIZE draws a ZONE row of pills beside FAMILY with the chosen zone lit; ADDRESS reads `<name>.<zone>`, or NO ZONE · CHOOSE ONE ON SIZE. COMMIT reads COMMIT · NO ZONE and stays dark until a zone is chosen, and its line names `--base-domain` only when the zone is not the session's default. Found live: COMMIT had sent `domain new quay` into a session with no zone. Also found live: the DOMAINS tile's paint now claims each domain's name for the language, so `domain <name>` typed in a fresh session opens the controller instead of running as text.
| 2026-10-23 (COMMIT writes the spec, then plays the tenant) | Epic #1056, docs/stratus.adoc D14. SIZE's COMMIT shows and sends two lines: `domain new <name> …`, then `play ~/tenant.mise --param NAME=<name>`, the manifest stratus ships in the session's home. The play is the controller's own line, so the build's billing question stands on its strip and its state events redraw BUILD, where COMMIT takes the screen once the spec is written; a refused spec leaves SIZE as it was, and a NO at billing leaves the spec written and nothing made.
| 2026-10-23 (`domain` takes the screen as the TD controller) | Epic #1056, docs/stratus.adoc D13 and D14, built whole against the mockup v2. In a stratus session `domain` (or `domain <name>`, a DOMAINS row, a domain's pill) opens the TD controller over ARGUS, trek's layout under the theme's palette: the upper gutter carries the STRATUS block and a pill per domain with + NEW; the header CONDITION (SIZING, ASKING, BUILDING, UP, SERVING, FAILED, GONE) with the billing line under it, the steps done, the class badge (BUILT HERE, ADOPTED · PLANS ONLY, CHECKOUT · READ ONLY) and the lock, then DOMAIN, COST, BUILD and SINCE LAST COMMAND; the lower gutter the views OVERVIEW, SIZE, BUILD, CLUSTER, CHRIS and TEARDOWN, and LEAVE. Every control sends a session line, shown in the domain log as typed (a domain's pill sends `domain <name>`; PLAN, VERIFY, SPEND; BUILD sends `domain build <name> --through run`; KILL stops following, then sends `kill <name>-build`; LIFT THE LOCK sends `domain protect <name> off`, a line of its own; TEAR DOWN sends `domain teardown <name>`), and a prompt at the foot takes any line. The engine's questions stand on a strip over the log: the billing question answered by YES · BILL or NO, never skipped; teardown's by the name typed back, DESTROY lit only when it matches. SIZE is the levers over what `domain size` read once: fit per zone and cost per hour redraw as they move, as presentation; COMMIT, held until the name is a free DNS label, sends `domain new` with what differs from the starting spec. Orange means money runs, red destroying. The controller reads `domain status` when it opens and after each line, and takes the `stratus.domain.state` events a build shows between; the tab's title carries the condition (`ASKING · quay`); a step finishing, the engine asking and a failure are heard. A build is not a game: LEAVE and Esc never stop one, and opening again finds it. Under 1250 px the controller does not take the screen, says so, and the line's answer prints as text. CLUSTER's nodes and projects and CHRIS's SMOKE AGAIN and ATTACH wait on the cluster in the tree, and say so. Proven by unit tests for the model and the screen, the smoke stage td-controller (desk and tablet sizes) over `stratus --fake`, and a fake build driven through the controller from SIZING to the billing question, YES · BILL, BUILDING with the running step, and UP. | `features/controller/model.ts`, `screen.ts`, `controller.css`; `app/tdController.ts`; `compositions/stratus/lang.ts`, `tiles.ts`; `app/main.ts`; `tests/smoke/smoke.mjs`
| 2026-10-22 (one login, a switch to the cloud) | stratus.adoc D14. The ChRIS login is the only login: a page that came through a porter switches to the same person's stratus session with `stratus oci` and back with `chris`, verbs of the frame (they act on the door, never on the session). The porter starts or rejoins the stratus session on the credentials filed under the person's ChRIS identity and moves its cookie; the page follows to the greeter while it boots, or straight to the mount. A session that may spend is asked for in words: `stratus oci --spend` asks for `spend` typed, and is otherwise read-only. A page that came in any other way refuses the verbs by name.
| 2026-10-22 (a pipeline's run lights its feed as a plugin's does) | Issue #1208, slice 2. A pipeline run answers a model of its own, `pipeline.scheduled` (every node it made, with each node's output folder and how it settled, and the nodes nothing in it runs off), instead of passing as one plugin's `run.scheduled` with the first node's id and an empty output path. The catalogue's RUN lights the feed the work landed in for either kind. `run.scheduled` gains `status`, CUBE's word as the run settled or `detached`. A variable is a session's, not a surface's: `set` answers a `variables.list` model and `set A` answers A's own model, which a variables pane can draw later; ARGUS draws none yet, and its console runs `A=$(…)`, `set` and `unset` as chell does.
| 2026-10-22 (DOMAINS says what each domain costs an hour) | Epic #1056, step 4c. The `stratus.domains` model gains an optional `cost` (an hour and so far, at list price, with its currency), filled by `domain status` from `spend`'s pricing of the domain's compartment. DOMAINS shows each priced domain's rate on its row, before the cluster's name so a narrow tile cuts the name and not the money (`harbor  09-run  $0.62/h  harbor.oci…`) and the domains' total among its figures (`$1.24/H`); a domain the daemon could not price, or an older daemon, shows none rather than a zero. The tile's words now say "and what it costs per hour", the extension the operator approved with them. Proven by the unit test and desk and phone screenshots over the live tenancy. | `compositions/stratus/tiles.ts`, `features/launcher/about.ts`
| 2026-10-22 (the HOST lamp names what each tier hands the daemon) | Epic #1056, step 4a. Calypso gains a fourth host-control tier, `tasks`: the engine may start long jobs that change a cloud and bill (docs/stratus.adoc, D10), named on its own and never granted by a bare `--host-control`. The amber HOST lamp already read the attach ack's tiers, but its title said `!` and pipes run on the daemon's host whatever the tiers were, which is false for a daemon granted `tasks` alone. The title now names each tier for what it hands the daemon, in the ack's order (`jobs may change the cloud and bill`); the lamp's text, `HOST <tiers>`, and law `host-control-is-annunciated` are unchanged. Chell's attach banner says the same per tier. Proven by a unit test and over a private `stratus --fake --host-control tasks` daemon. | `app/status.ts`
| 2026-10-21 (a stratus dashboard leads with its domains) | Epic #1056, step 3. A session over `stratus` has tiles of its own (docs/stratus.adoc, D11), asked of the session at paint time from the commands a terminal runs: DOMAINS leads in the wide seat with each Trusted Domain from `domain status` (its last finished build step and its cluster; a domain with a step gone wrong, such as an expired ISO link, in red, with a NEEDS A LOOK figure), a row opening the domain's folder; CAPACITY follows FILES with `capacity` (per zone, free of permitted, for the shape families the domains' specs name; nothing free in red; with no family named it teaches the line `capacity E5`). FILES says what a stratus home holds. Both read the wire's new models (`stratus.domains`, `stratus.capacity`) rather than the rendered text. The two tiles' descriptions are drafts for the operator's approval. Hourly cost on DOMAINS waits on how `spend` is run. Proven by the unit test, the smoke stage stratus-tiles (and frame-alone, which now counts the frame's five tiles among a composition's own) over `stratus --fake` and `calypso --backend null`, and desk and phone screenshots over the live tenancy. | `compositions/stratus/tiles.ts`, `features/launcher/about.ts`, `app/main.ts`, `tests/smoke/smoke.mjs`
| 2026-10-21 (a frame-alone session boots inside the frame) | Epic #1056, follow-up 2. Over `stratus` (and `null`) ARGUS booted with the dashboard alone on the screen: header, gutter and console zoomed away, so the identity and the prompt the session exists for were off stage until Esc. The ChRIS landing law (2026-09-17, a session begins at the dashboard) zooms only on a browser's first visit, and a frame-alone daemon serves on a fresh port each start, so its browser storage is always a first visit and every boot hid the frame. A frame-alone session now boots with the dashboard on stage inside the whole frame: the header with its brand mark (the TD mark under stratus), the console drawer open at its prompt, the gutter. The DASHBOARD gutter still zooms it alone; a ChRIS session is unchanged. Proven over a private `stratus --fake` daemon by the smoke's boot check and the frame-alone stage, and by desk and phone screenshots. | `app/main.ts`, `tests/smoke/smoke.mjs`
| 2026-10-21 (a session over stratus wears the Trusted Domain's brand) | Epic #1056, slice 1b. ARGUS asks the daemon its backend and draws ChRIS's composition only for `chris`; a session over `stratus` now has a composition of its own, brand only in this slice: the Trusted Domain's North Star in the driver's ring (OCI: Oracle red) as the header's mark and the tab's, the same mark as the console's splash, and the credit line "a Trusted Domain console". What the composition owns in the page's markup carries `data-composition="stratus"` and stands only under it, as ChRIS's does. The tiles (DOMAINS, CAPACITY, TENANCY, STEPS), the domain pane and the controller are later slices of docs/stratus.adoc. | `index.html`, `app/main.ts`, `lcars/argus.css`, `assets/brand/td-oci.svg`; the mark from `@fnndsc/menu/logo/td`
| 2026-10-21 (the prompt bar draws a backend's segments) | Epic #1056, slice 1b. The wire's prompt context gains backend-neutral segments (docs/stratus.adoc, D9): a key, a label, a value and a tone each, filled by the backend in display order. The console's bar draws them in that order, a glyph by key (cloud, tenancy, region, user, domain, working directory) and a colour by tone, with its own PHYSICAL, duration and status telemetry trailing; a long identity, an e-mail address at a tenancy, is cut from the left at a fixed width and carries its whole value as a title. A ChRIS daemon sends no segments and its bar is drawn from the named fields exactly as before. The bar is its own pure module, `console/promptBar.ts`, tested by value; the glyphs and palette stay in `terminal.ts`, where `lint:prompt` holds them to chell's. | `console/promptBar.ts`, `console/terminal.ts`, `scripts/prompt-parity.mjs`
| 2026-10-19 (the moving bridge is proven) | Epic #1172, slice S7. The `trek-motion` smoke scenario, on both lanes, on seed 13 (a Klingon in the start quadrant): the standing order fills the long-range scan at the first prompt with its lines folded in the log, the Klingon wears the same mark on the scan, the tactical wheel and its target pill, a torpedo fired at it flies the printed track and the frames the picture played are counted against the log's lines kind by kind (torpedo points, kills, novas, moves, escapes, hits: every frame a line, the track's frames the printed points, the kill's burst after the last point, the scan agreeing with the state once the picture settles), and a view pill pressed during a quadrant crossing lands the ship in the new quadrant at once with the log whole. Two laws land with it: a mark means one thing everywhere (a lint that the scan, the wheels, the target pills and the legend all draw from the one symbol table and that no sector is drawn as its glyph) and a frame replays a printed line. | #1172; apps/argus/tests/smoke/smoke.mjs; scripts/aegis-lint.mjs
| 2026-10-19 (the bridge fits its window) | Epic #1172 follow-up. The stars breathed off their cells: a `<use>` placed by x,y cannot be scaled in place, every browser scales it about the drawing's corner, so a far star slid most of a cell toward 0,0 and back. Every mark is now a group that carries its place with the symbol drawn about the group's origin, and whatever scales or turns a mark (breathing, a heading, the slide) acts about the mark itself. The header keeps its values at a narrow window: three groups share the width, what moved since the last command sits under the mission, and the condition block and label pills are narrower. The left column is the long-range scan and the chart, the chart sized to the room left (engineering has its own view); a slider's ticks thin to every other and then to the ends when the track is narrow; the bridge's own lines in the log carry the prompt they answered (`Command: m`, `Course: 236`) and are set apart from the engine's. | #1172; apps/argus/src/features/trek/marks.ts; apps/argus/src/features/trek/trek.css
| 2026-10-20 (a question shows where it is answered) | Epic #1172 follow-up, from the operator's tablet. A station column shorter than its stations held a question's lit control below the fold: the torpedo's burst angles waited unseen, and the operator read it as no way to fire. A question opened now scrolls its lit control into view. Phasers cannot fire through raised shields (the engine's Sulu refuses), so the PHASERS pill stands hollow while they are up and its title says why; the engine still answers the press. The stacked-slider rule of the day before had also misplaced pill-row labels under it; scoped to the slider's own children. | #1172; apps/argus/src/features/trek/bridge.ts; apps/argus/src/features/trek/stations.ts
| 2026-10-19 (the bridge moves) | Epic #1172, slice S6, from the operator's grill. The rule: nothing on the screen moves without an engine line or a change of state behind it, and the frame is the clock. The engine's lines become frames in their order (`features/trek/motion.ts`), each starting with its cue, and the ship's log scrolls with the picture; the readouts and the controls take a state at once, the picture takes it in its turn. A torpedo (an orange-red photon with a white heart and flickering pulses, its own colour under every palette) flies the printed track point by point at 140 ms a point, trailing a comet tail; a Klingon flies its `moves to` line on a red track at 170 ms a sector, keeping its mark, or off the edge toward the quadrant it escapes to, whose long-range cell blooms red; a kill, a nova (twelve spikes, the star's colour), a misfire (red, short) and a supernova burst at their sectors after the ARGUS node flare; a phaser beam joins the ship to the Klingon a hit line names; a Klingon's `HIT` line beams from its sector to the ship, flares the shield ring and takes the absorbed share off it (trunc(hit × absorb%), the engine's own arithmetic), and when the line names no sector (the short-range sensors damaged) the ring flares with no beam. A move lays a dashed track from where the ship was drawn to where the state puts it, a pulse runs it as the orrery's tubes carry theirs, and the ship slides with it, turned to the way it went; a crossing runs the ship off the edge where the straight line leaves the quadrant, dims and rescans the grid, blooms the long-range cell that becomes HERE, and runs it in from the matching edge; an obstacle stop ends where the state says. A command pill, a view pill or Esc skips every pending frame to its end state at once (the log catches up whole), and reduced motion plays every frame that way. Four cues join the voice, `nova`, `supernova`, `klingon-pass` and `shield`, aliased in the bank to the nearest existing models until their own are chosen. Verified live on an isolated daemon in a headless ARGUS on seed 13: a torpedo at the Klingon flown over its ten printed points with the tail drawn, the star's nova and the kill bursting in the log's order and the Klingon gone from the scan; a move west of 0.3 sliding the ship off the edge, the grid dimmed, the long-range cell 7,5 blooming, the ship arriving at 7,5 sector 9,8 pointing 270°, nothing left on the motion layer; the frame tally read through `__argusTrek.frames()`. The proof with the laws (S7) follows. | #1172; apps/argus/src/features/trek/motion.ts, bridge.ts, marks.ts, cues.ts, trek.css; apps/argus/src/app/trekBridge.ts; apps/argus/public/sound/bank.json
| 2026-10-19 (the bridge draws marks) | Epic #1172, slice S5, from the operator's grill (the mockup linked there). The C's letters leave the short-range scan: it is one picture now, each sector a pressable cell (`role=button`, walked by the arrow keys) carrying a symbol for what it holds, the ship's mark turning to the course last committed and wearing a shield ring read from the state (width and colour the shield over its maximum; thin and dashed when down; the starbase's when docked), each Klingon wearing one of four marks and facing the ship. The engine's Klingon list has no ids, so a Klingon takes its mark when first seen at a sector and the engine's own lines (`moves to`, `escapes`, `destroyed`) carry it; with the short-range sensors damaged the engine prints no lines and the marks follow list order. One symbol table feeds the scan, the wheels' rims, the target pills and a legend beside the C's glyphs (which stay in titles and in the log, the engine's own voice). The charts leave an unscanned quadrant empty, mark a supernova with the radiation trefoil, and the long-range scan shows the edge of the galaxy as such; the stars breathe, the one motion with no line behind it, off under reduced motion. A standing order: the bridge sends the engine's own `l` at the first `Command:` prompt and on arriving in a new quadrant, while the long-range sensors work, its lines folded in the log under one dim head; between arrivals the codes keep themselves, because `klmove` raises the quadrant a Klingon flees to. Verified live on an isolated daemon in a headless ARGUS on seed 13: a Klingon in the start quadrant drawn with its mark on the scan, the wheel and the target pill, the long-range panel filled by the standing order with the edge row below, the legend's seven kinds, a pressed sector outlined, SET COURSE turning the ship's mark to 90°, a hit taking the ring from 92% to 89%, the arrow keys walking the scan. Motion (S6) and its proof with the laws (S7) follow. | #1172; apps/argus/src/features/trek/marks.ts, bridge.ts, controls.ts, stations.ts, model.ts, trek.css
| 2026-10-19 (trek from the command line) | Epic #1157. The operator asked how to start trek in ARGUS, and under the terminal console (every deployment's) there was no way: `trek` typed at the prompt is the pty's and plays as text. The ARGUS language gains a `trek` subject: `trek [--seed <n>]` on the command line (prefix-`:` or the LANG capsule) opens the bridge through the host, from either console; the surface claims only a game, so `trek --credits`, `--help` and `--version` fall through to the session as every other unclaimed line does (surface-never-shadows-the-session). The `trek-bridge` smoke scenario now runs on both lanes, entering by the command line under a terminal. | #1157; apps/argus/src/console/argusLang.ts; apps/argus/src/app/consoleHost.ts, main.ts, trekBridge.ts
| 2026-10-19 (the bridge is proven) | Epic #1157, slice S4. The smoke suite's `trek-bridge` scenario (the transcript lane's own, since a terminal console runs `trek` as text): `trek --seed 42` typed at the console, the page measured hidden behind the bridge, the setup answered by pills while a MOVE pill pressed early presses nothing, the state drawn (condition, 100 sectors, one ship, 64 quadrants, the score), a pressed sector a tenth of a quadrant, a move through the engine, HELM and ENGINEERING drawn, SCORE answered through the engine on COMPUTER, the voice's bank and models fetched with cues played, Esc asking, STAY staying, LEAVE ending the game and the console READY with no question left open. Four laws land with it: a program may take the screen, a program gives the screen back (a lint on the one attribute, its `finally`, and the stylesheet), a bridge answers the prompt in front of it, and a pill that acts is heard as the thing. | #1157; apps/argus/tests/smoke/smoke.mjs; scripts/aegis-lint.mjs
| 2026-10-19 (the page has a voice) | Epic #1157, slice S3c. Every sound ARGUS makes is synthesized in the browser: a cue is a spectral model of a reference clip (partial tracks and thirty-two noise bands on an 11.6 ms grid, made by `tools/trek-sound`), played by one oscillator per track and a band-pass noise voice per band through Web Audio; no recording ships. The bank (`public/sound/bank.json`, sixty-four models) names a cue for each event and one computer beep for each of the bridge's pills. The frame's own four beeps go through the voice too; a deployment that keeps recordings (`PORTER_SOUNDS_DIR`) still hears them, because the porter now names its folder's files at `sounds/index.json` and the page plays those as recordings. The bridge is heard by rule: a pill that only opens, sets or answers beeps; a pill that does something is heard as the thing (ENGAGE is warp or impulse; a phaser or torpedo is heard when the engine says it fired; ABANDON, DESTRUCT and COMPUTER are the transporter, the signal and the computer thinking); the game's events come from the state (the alerts, docking, a new quadrant's sensors) and the engine's own lines (an attack, a hit, a kill, a starbase answering), so one kill is one explosion. The audio pill is the one mute. Verified by an offline render of the player (the same code, through node-web-audio-api) and live in a headless ARGUS: the bank and models fetched, cues played and oscillators started as the bridge was driven. | #1157; apps/argus/src/lcars/synth.ts; apps/argus/src/features/trek/cues.ts, bridge.ts, stations.ts; apps/argus/src/app/main.ts, trekBridge.ts; apps/argus/public/sound/; apps/porter/src/app.ts; tools/trek-sound/
| 2026-10-19 (the bridge's stations) | Epic #1157, slice S3b. The gutter's four other views draw: HELM and TACTICAL enlarge their stations beside the short-range scan (the same station built twice, once small for the BRIDGE view, once large; a sector pressed points every wheel); ENGINEERING is the damage control report whole, each device with its repair times in flight and docked as `dcrept` prints them, the ship's reserves as gauges, what changed since the last command, and REST with a preview of what repairs complete in that time; COMPUTER is the galaxy chart large, the computer's nine requests as pills, and panels that work the computer's arithmetic live (course and distance to a pressed quadrant and sector, warp and impulse cost, trajectory with phaser effect, the score so far) beside the engine's own answer. A request pill at `Command:` sends `c` and answers `Request:` itself; the follow-up questions are answered from the panels, which are the controls. Manual phasers play at TACTICAL bank by bank, with FIRE THE BANKS SET sending the bank of 0 that ends them. A station's head names the step and shows CANCEL when the C has a "never mind" for the open question. The score stands in the header. Found on the way: S3a sent a pressed sector's distance in sectors where the C reads quadrants (one press moved the ship ten times too far); the frame's bars stood apart from the gutter, so the elbows are now joined (the top gutter's fill sweeps into the header's bar, a cap above the views into the body's). Every mirror of the C's arithmetic is tested against the engine's own print for the same request. Verified live on an isolated daemon in a headless ARGUS: each view visited, a move of 0.3 quadrants costing 0.12 stardates, the computer's SCORE, COURSE (the panel's 139° matching the engine's) and WARP COST answered through the engine, Esc and LEAVE returning the console READY. The sound bank (S3c) and the smoke scenario with the laws (S4) follow. | #1157; apps/argus/src/features/trek/stations.ts, controls.ts, bridge.ts, model.ts, trek.css
| 2026-10-19 (trek takes the screen) | Epic #1157, slice S3a, from the operator's grill (docs/trek.adoc). `trek` typed at the transcript console opens the bridge over ARGUS: a program that takes the screen and gives it back when the game ends. The bridge is a player at a terminal, not a rewrite of the game: it runs the session's `trek` as the operator's own command (never instrument traffic, which the daemon refuses a question), reads the state the engine shows before every `Command:` prompt (the `event` message, S2), and answers the engine's questions one at a time from controls lit by the question: command pills, a course wheel, sliders for distance, warp factor, rest and phaser units, pills for choices, a strip for the questions the engine asks on its own. Nobody types; the ship's log is the game's own words, read only. Esc asks before leaving; LEAVE BRIDGE abandons the open question, which the engine reads as end of input and ends the game as the C does, and the console's outcome is the game's own, not an error. The bridge draws its own LCARS frame because the takeover removes ARGUS's, in the theme's palette. Found on the way: the engine's styling wrapper dropped `state_show`, so a styled game showed no state (bsd-trek fix, with a test). Verified live on an isolated daemon in a headless ARGUS: setup answered by pills, the state drawn (condition, 100 sectors, the ship), MOVE then a sector pressed, SET COURSE and ENGAGE moving the ship (stardate 2100 to 2104, SINCE LAST COMMAND showing the cost), Esc asking, LEAVE closing the bridge and the console returning READY; unit tests for the model, the harness and the bridge. The station views and the sound bank are the slices after this one; the laws land with the smoke scenario (S4). | #1157; apps/argus/src/features/trek/model.ts, harness.ts, bridge.ts, trek.css; apps/argus/src/app/trekBridge.ts; apps/argus/src/app/main.ts; apps/argus/src/calypso/client.ts; packages/trek/src/host.ts, trek.ts; packages/brasa/src/builtins/games/trek.ts
| 2026-10-17 (a host program gets a terminal) | #1096, deferred from the console epic, made simple by it: a terminal's chell already runs on the daemon host under a pty. Under host control, `!cmd` typed in the terminal now runs as that chell's child, in the pty, instead of on pipes with no terminal: `vi`, `less`, `top`, and the operator's own login shell (`!zsh`, prompt theme and all) work, and Ctrl-C reaches the child as a terminal's interrupt. While the child runs, chell sets its key readers aside and leaves the terminal cooked, so the child alone reads the keys; it ignores the interrupt meanwhile, and a Ctrl-C pressed before the daemon's request arrives keeps the shell from starting at all. STOP in terminal mode sends the terminal a Ctrl-C as well, for a child that honours it. Without host control nothing changes: `!` from a browser's terminal is still refused. A line typed in the transcript still runs `!` on pipes, which has no terminal to give. Verified on a private daemon with host control: `!vi` full screen, edited and saved, `!zsh` with the operator's own prompt, `exit` back to chell; `!sleep 30` ended by Ctrl-C and by STOP in about 0.2 s. | #1096; `app/consoleStop.ts`, `app/main.ts`; calypso `daemon/launch.ts`, `daemon/server.ts`; chell `core/cliSurface.ts`, `core/lineDiscipline.ts`, `core/repl.ts`
| 2026-10-17 (STOP ends what runs) | From the operator, after `!vi ~/.yarn` under host control hung the session past Ctrl-C and had to be killed on the host. The host shell ran `vi` with no terminal and no way to kill it, and the command held the session's lane, so everything queued behind it. Now every command can be stopped: Ctrl-C, or STOP, an alarm capsule in the console's drawer that stands only while something of this page's runs (the bar carries no machinery). Either aborts the command's signal; a host process it started is killed (its process group: SIGTERM, SIGKILL after two seconds); a command that does not end when asked is abandoned after three, the session freed, its late output dropped and its questions refused. STOP reaches the page's own commands and those typed in its terminal, never its silent housekeeping, and a STOP pressed while the operator's command waits its turn ends it unrun. A chell that goes away hangs up on its command, as a terminal's SIGHUP does; a browser that reloads does not. In the terminal, chell reads Ctrl-C from the raw bytes while a command runs, since a host program's output can make the terminal answer into its input and swallow the key. Verified on a private daemon with host control: `!sleep 30` and `!vi` stopped by STOP and by Ctrl-C, the prompt back in about 0.2 s (vim, which ignores SIGTERM, in two); the lane free after each. | #1125 follow-up; `app/consoleStop.ts`, `calypso/client.ts`, `index.html`, `lcars/argus.css`, `tests/smoke/canon/`; calypso `daemon/server.ts`, `daemon/hostControl.ts`, `daemon/launch.ts`; brasa `core/cancellation.ts`, `core/engine.ts`, `core/dispatch.ts`; chell `core/repl.ts`; menu `messages.ts`, `surface.ts`
| 2026-10-17 (a game plays where it is typed) | Epic #1125 follow-up, from the operator: with the console a terminal, the showpieces and the arcade (`sl`, `cmatrix`, `rain`, `asciiquarium`, `tetris`, `snake`) play in it, and `--pane` draws them in the GAMES pane as before. The programs (a state and a picture on a cell grid) move from ARGUS to `@fnndsc/menu/games`, so the pane and chell run the same ones. On a `games.show` model chell takes the terminal: the alternate screen, the cursor hidden, the grid drawn in the console's hues a tick at a time, the keys read raw with readline set aside, and q, Esc or Ctrl-C gives the screen back; it marks the console `playing` (OSC 6677) while it has it. ARGUS opens the GAMES pane only for `--pane`, or where the deprecated transcript is the console, which has no screen to play on. A chell whose output is not a terminal prints the still the kernel sends. Verified on a private daemon: `cmatrix` and `tetris` played in ARGUS's terminal and gave the prompt back, no pane opened; `cmatrix --pane` opened the GAMES pane; the games stage of the smoke 14 ok in both console modes. | #1125; `app/main.ts`, `features/games/panel.ts`, `console/terminalScreen.ts`, `tests/smoke/smoke.mjs`; menu `games/`; chell `core/gamePlayer.ts`; brasa `builtins/games/show.ts`, `builtins/help.ts`
| 2026-10-16 (the transcript is a fallback) | Epic #1125, slice 4, from the operator's grill. The transcript was never a terminal: it sent each line to the daemon's engine and drew the envelopes that came back as a log of blocks, with no screen to address (why full-screen programs needed the GAMES pane). The terminal replaces it as the console, and nothing chooses otherwise: the CONSOLE capsule, `console terminal\|transcript` and the device's stored choice are gone (a choice an earlier release kept is forgotten). Grilled: retiring the transcript outright would leave a host where node-pty did not build (it compiles on Linux, needing python, make and g++) with no console at all, and node-pty stays optional so an install never fails on it; so the transcript stays as a deprecated fallback, frozen, shown only where the daemon offers no terminal, and its first line says so and why. Its plumbing (the page's own verbs, notes and pane questions, mirrored into the terminal) stays either way. `CALYPSO_NO_PTY=1` makes a daemon that host; the nightly's transcript pass runs its own scenarios against one, and a terminal-only scenario holds the terminal's questions (asked and answered in line, Ctrl-C abandons, a pane's answer lands in the scrollback). Verified on a private daemon: under `CALYPSO_NO_PTY=1` the console was the transcript with its deprecation line, its stages 31 ok; with a pty, the terminal and no capsule. | #1125; `app/consoleMode.ts`, `console/argusLang.ts`, `index.html`, `tests/smoke/smoke.mjs`, `tests/smoke/canon/`, `docs/architecture.adoc`; calypso `daemon/launch.ts`; `scripts/smoke-nightly.sh`
| 2026-10-16 (the console is a terminal) | Epic #1125, slice 3c. The console is the terminal wherever the daemon offers one; TRANSCRIPT on its drawer goes back, and a device that chose the transcript keeps it. Before the default moved, the smoke learned to drive the terminal: `SMOKE_CONSOLE=terminal\|transcript` picks the console, every scenario types through one input that is the transcript's field or the terminal's keys, reads what the console says since a mark (counted in line feeds, so it holds as old lines scroll off), and waits for the chell's own state, which chell marks in its output (`OSC 6677 state=idle\|busy\|asking`, only in a terminal's chell) and ARGUS sets on the console as `data-terminal-state`. Two scenarios that read the transcript's ask blocks stay transcript-only, and the nightly runs the console scenarios a second time in transcript mode. A question the operator abandons with Ctrl-C in a terminal is abandoned, as Esc abandons it in the transcript. A question answered on a pane is mirrored into the terminal's scrollback with its answer. Found while porting and closed: a terminal's chell runs on the daemon's host, so a `!` line, a pipe segment, an editor or a delivered file asked of it would have happened on that host; they now go to the browser that owns the terminal, which does them its own way or refuses by name, and with host control granted the session's own host shell is unchanged. A line typed in the page's own terminal moves the page as one typed in the transcript did (`say` speaks, a game opens the GAMES pane, a moved working directory re-lists the panel). Verified: on a private daemon, the console stages and every stage a failure was found in passed in both modes (55 ok in transcript; terminal clean but for one file-download timing flake that passed on rerun); the full suite, ported, runs at the epic's end. The smoke port also mended what the terminal exposed in the suite itself: uploads now wait for the cd they land in, a RUNS pane behind another is backed out of its graph, the universe's FEEDS count is read as a number (the escape had been lost, so 660 FEEDS read as nothing), and a scenario waits for the console to come to rest in transcript mode too. And one session bug: REFRESH on the roster pressed during a roster delta was absorbed into it and never walked the index (salsa , now forced through) | #1125; `app/consoleMode.ts`, `console/terminalScreen.ts`, `console/terminal.ts`, `calypso/client.ts`, `app/main.ts`, `console/argusLang.ts`, `tests/smoke/smoke.mjs`; chell `core/lineDiscipline.ts`, `core/repl.ts`, `core/cliSurface.ts`; calypso `daemon/server.ts`; salsa `vfs/providers/proc.ts`; `scripts/smoke-nightly.sh`
| 2026-10-16 (the console can be a terminal) | Epic #1125, slice 3b, from the operator's grill. A CONSOLE capsule on the console's drawer (reading as its next action: TERMINAL, then TRANSCRIPT) makes the console a terminal: xterm.js drawing the chell the daemon runs for this page under a pseudo-terminal, in the console face and the console's sixteen ANSI colours on black, so it is the same shell as a terminal's (its prompt, line editing, history, completion, questions in line with their output, full-screen programs). `console terminal` and `console transcript` press it; the choice is the device's; a daemon without a pty shows no capsule. The tab keeps its terminal's id, so a reload reattaches and replays. A touch device gets a key row (ESC, TAB, CTRL, arrows). The transcript keeps its record behind the terminal. What ARGUS itself runs or says (a pressed verb's line, its streamed output and result, a note) is mirrored into the terminal as notes above chell's prompt, and the daemon skips that command's session echo to this page's own consoles, so nothing prints twice. Grilled after the operator caught it: a pressed verb's question still stands on the pane that was pressed, console open or closed (handing the line to the terminal's chell would have sent the kernel's question to the terminal, unseen with the console closed); a question with no pane to stand on is a sheet over the stage. The default stays TRANSCRIPT until the smoke drives the terminal (slice 3c). Verified live on a private daemon: the capsule switched; `pwd` and `help keys` (an ARGUS word, routed back to the page) answered in the terminal; trek played with its questions in line (`Command: lrscan`); a reload reattached the same terminal with the game on screen; the console stages of the smoke ran 18 ok in transcript mode; both canons gained only the capsule. | #1125; `console/terminalScreen.ts`, `app/consoleMode.ts`, `console/terminal.ts`, `calypso/client.ts`, `app/main.ts`, `app/asks.ts`, `index.html`, `lcars/argus.css`
| 2026-10-16 (the surface's words reach every shell) | Epic #1125, slice 3a, from the operator's grill. The console is to become a terminal whose keystrokes go to a chell, and a chell never saw ARGUS's own language: `dag %1 census on`, `desktop show`, `view runs` were read in the browser before a line reached the wire. Grilled A of three (a keystroke shadow, or the language moved out of the console, were the others): ARGUS declares the subjects its language claims, on attach (`surfaceClaims`; whole subjects bare, a shared one with its claimed verbs, as `argusLang`'s own tables say), and the daemon routes a single command in one to the browser (`surfaceLine`, answered by `surfaceLineResult`), never to the engine. The browser that owns a terminal answers its chell's lines first, else the most recently active; with none attached a subject claimed whole is refused by name and a shared one stays the session's. Completion offers the claimed words; a browser's note reaches its own consoles' chells, printed above the line being typed. Law surface-never-shadows-the-session, now held by the daemon in one place. Verified live on a private daemon: `desktop show` in a terminal chell printed the browser's desktop script, `help keys` its chord table (`help ls` stayed the session's), `view runs` put RUNS on the browser's stage, and with the browser gone `dag census on` was refused by name. | #1125; `calypso/client.ts`, `console/argusLang.ts`, `app/main.ts`; calypso `daemon/claims.ts`
| 2026-10-16 (an open question shows) | Epic #1128, slice 4, from the operator's grill. A command waiting on a question no longer holds the session (slices 1-3), so an open question can go unnoticed: a game paused in another tab, a confirm left in a terminal. The status strip shows `? N WAITING` in the annunciation amber while any surface in the session has a question open, each named in its title (which surface asks what, and for how long); the lane instrument adds an ASKING row (how many, the oldest, its age); `who` says what each surface is waiting on; chell's prompt carries a `? N waiting` segment. The daemon owns the count (its open questions ride the heartbeat's lane and every prompt context), so every surface reads the same fact. Verified live on a private daemon: trek holding "What length game:" in a terminal showed `? 1 WAITING` on the strip and `? What length game:` against that surface in `who`, and cleared when answered. | #1128; `app/status.ts`, `app/laneInstrument.ts`, `app/main.ts`, `index.html`, `lcars/argus.css`
| 2026-10-15 (the console's face holds a grid) | Epic #1125, slice 1, from the operator's grill. The console is to become a terminal (xterm.js on a daemon-side chell pty), and a terminal lays text on a fixed grid whose cell is the first font's width: a glyph from a fallback is squeezed into that cell. Share Tech Mono, the console's face since the prototype, carries none of the prompt's powerline segments or Nerd Font icons, so every one of them would come from MesloLGS NF and be cropped. The operator asked for an LCARS-like face or JetBrains Mono's Nerd Font. LCARS display faces (Antonio, Okuda-style) are proportional and cannot sit on a grid. ShareTechMono Nerd Font (Nerd Fonts v3.5.1, renamed ShureTechMono under the OFL) was measured and lacks arrows, ✓ ✗ ● and the trek scan glyphs, with a 0.54 em cell that would crop each Meslo fallback. JetBrainsMono Nerd Font Mono was measured and holds every glyph the prompt, the listings and the scans use at one cell of 0.60 em, Meslo's own width, missing only the prompt's v2 microscope and ★, which fall back to Meslo without cropping. It is the console's face now, bundled as WOFF2 with its OFL text, MesloLGS NF behind it. Verified live on a private daemon: the prompt line, `ls` with its kind icons and the powerline segments set on one grid; both canons re-recorded on the console's records only (font, and the prompt bar's line height from 18 to 21 px). | #1125; `lcars/argus.css`, `lcars/fonts/`
| 2026-10-15 (a view travels, safely) | Epic #1081, slice 3 (#1087), built to the operator's grill. A view now travels two ways: a link (`#view=` and the script, URI-escaped and readable; COPY LINK on the share sheet) and a file in ChRIS (`desktop load <path>`; the operator's own under `/home/<them>/` runs as their own). A received view is a script in the receiver's name, and the console also reaches `rm` and `pacs pull`: it runs only view lines (`VIEW_LINES_ALLOWED`), shows the lines and asks RUN or NOT NOW, and a view with any other line is refused whole, the line marked with why. Its display modes hold for the visit (`feedModes_hold`; found live: held writes alone were undone when the graph arrived and re-read the device's store, so a held view's modes now stand in memory and are read first, until the operator's own next press). The replay stops at the first line it cannot do and says so. A files pane records its folder (`file %n open <path>`, a new verb that lists it rooted). Verified live on feed 4389: the sender's view (2D, CENSUS, GRAVITY, job 620500) went by link to a receiver kept in SHAPE and 3D, asked, ran, and stood in 2D, CENSUS, GRAVITY on 620500 while the receiver's kept CENSUS stayed off; a crafted link ending `rm -r ~/precious` was refused with RUN withheld. | #1087; `console/argusLang.ts`, `app/shareView.ts`, `features/feed/view.ts`, `app/consoleHost.ts`, `app/main.ts`, `lcars/argus.css`
| 2026-10-15 (a view is shared from a baseline) | Epic #1081, slice 2 (#1084). A desktop script replays from whatever is on screen, and its `%n` pane names resolved only inside `desktop load`: typed or pasted into the console, `dag %1 census on` named no pane and did nothing (verified: a replay by hand set only GRAVITY, which the pane already wore). Now `desktop clear` is the baseline: the operator's own screen kept first as a PANES card (grilled: a colleague's view must never cost the receiver what they had up), then every pane closed and the dashboard on stage, where every session begins, and the pane numbering a load uses starts there, so the lines after it work loaded, typed or pasted. SHARE VIEW, a header pill beside AUDIO (and `desktop share` in the console), composes the stage as `desktop clear` and then the desktop's lines, stands a sheet over the stage with the script, COPY and CLOSE, and copies it at once — COPIED, or SELECT + COPY with the script selected where the browser has no clipboard. Verified live on feed 4389: the sheet showed the view through `dag %1 select pl-dicom_repack_620500`; with the modes changed, the script replayed by hand from `desktop clear` put back 2D, CENSUS, GRAVITY and job 620500. | #1084; `app/shareView.ts`, `console/argusLang.ts`, `app/main.ts`, `index.html`, `lcars/argus.css`
| 2026-10-15 (a view is a script) | Epic #1081, slice 1 (#1082). The operator: a collaborator should be able to send a script that puts another user in the same place and visualization. Desktops already captured the stage as console lines, but the language could not say the whole view: PULSE, GRAVITY and SPHERES/STARS had no line, CENSUS and PULSE only toggled (a replay on a device already in CENSUS took it out), nothing selected a job or entered it, and `desktop show` (its own serializer) wrote only layout, projection and hue and never which feed was on the graph. Now every mode has a line that sets it (`dag pulse|census|gravity on|off`, `dag draw spheres|stars`, the rest as before), `dag select <feed or job>` lands the pane through JUMP TO's own path, and `dag enter <job>` presses ENTER NODE once the job is selected. A desktop, through `desktop show` and the PANES cards alike (`view_lines`), states every mode and where the graph stands, by name (`dag %1 select pl-dicom_repack_620500`). Verified live on feed 4389: the lines set the view, `dag enter 620500` opened the job's folder, `desktop show` printed the whole view, and `desktop load` after changes put it back. Law a-view-is-a-script. | #1082; `compositions/chris/lang.ts`, `features/dag/panel.ts`, `app/consoleHost.ts`, `app/main.ts`
| 2026-10-15 (a transfer row never freezes) | On titan the operator pressed CANCEL on a large download and the row read CANCELLING with nothing after. Not reproduced on an isolated daemon, where a cancel ends CANCELLED both during CUBE's first-byte wait and with bytes flowing; the difference on titan is the door between page and daemon. What the row did wrong regardless: it waited on every answer with no deadline, so a session that stopped answering froze it silently. Now each ask has a 5 s deadline (`TRANSFER_ASK_MS`); three silent asks read LOST TOUCH WITH THE SESSION · STILL ASKING, a cancel with no answer reads THE CANCEL GOT NO ANSWER FROM THE SESSION · PRESS CANCEL AGAIN, a session that knows no such download says to cancel it in the browser, and while a cancel is out the row reads CANCELLING beside the live count. Found reading it: a cancel while the daemon waited on CUBE was overwritten when CUBE answered, and the browser's save began anyway; a cancelled download now stays cancelled (`DownloadLedger.cancelled`, the save refused 410, read as `cancelled — nothing was saved`). | #1078; `features/files/transfer.ts`, `features/files/download.ts`, calypso `daemon/downloads.ts`, `daemon/server.ts`
| 2026-10-15 (a pane opened on a place says when it cannot list it) | Testing links, a FILES pane opened by a link stood blank forever: the session it ran on had started disconnected from CUBE, and the pane's own `ls` failed with nothing said — AWAITING LISTING, an empty bar. (On a connected session the same cold link lands and lists in about 15 s.) A pane listing a place of its own (a link's landing, a rooted walk, BACK) now checks that the place came; when it did not, the bar reads COULD NOT LIST and the session's words (`refusalWords_extract`), the field says the same instead of awaiting, and the console notes it. Verified live with a link to a folder that does not exist: `COULD NOT LIST · CANNOT LIST /HOME/…/NO-SUCH-FOLDER: NO SUCH FILE OR DIRECTORY`. | `app/browser.ts` (`rootedListing_show`), `features/files/panel.ts` (`listing_fail`)
| 2026-10-15 (a download says how far it has got) | The operator and Sandip pressed DOWNLOAD on a 3.2 GB file, on a desktop and a tablet: nothing happened that either could see, and the download seemed never to start. Measured on an isolated daemon: CUBE took 16 s to answer, ARGUS showed nothing while it waited, then for a large file dropped that answer and handed the browser a second request, which waited on CUBE again before the browser's own download list showed anything; the only word was a console line at the end. CUBE was then serving about 300 KB/s, over two hours for 2.6 GB. Now a download carries an id. ARGUS probes (HEAD, `probe=1`); the daemon opens the file, answers its headers and holds the stream (`DownloadLedger`, `downloads.ts`) for the save that follows, so CUBE is asked once; a file under 256 MB is held in the page and saved from there, a larger one is handed to the browser. The daemon counts every byte it passes under the id and answers `/vfs?progress=<id>` and `cancel=<id>` on the route porter already carries. A transfer row stands at the press, at the foot of the screen, and reads the count each second: WAITING FOR CUBE, WAITING FOR CUBE'S FIRST BYTE, bytes of the whole with percent, rate and time left, DONE, FAILED with CUBE's own reason, or CANCELLED; CANCEL cuts the stream and the browser reports a failed download. Verified live on Sandip's 2.6 GB file: the row stood at once, the browser took the held stream, CUBE's first byte came at 14 s, the count ran at about 300 KB/s with time left, and CANCEL ended it. Law a-download-says-how-far-it-has-got. | #1076; calypso `daemon/downloads.ts`, `daemon/server.ts`; `features/files/download.ts`, `features/files/transfer.ts`, `app/browser.ts`, `lcars/argus.css`
| 2026-10-15 (a link is in sight) | The operator pressed LINK on the RUNS rail and nothing said anything; only the tablet's own toast ("Copied to your connected devices") gave it away. The press had written LINK COPIED into the bar's state slot, which the graph's liveness (SETTLED) rewrites within a second, and the pill lived on the mode rail, out of sight whenever the rail was retracted. The operator's design: the link stands under the place's name as a pill, showing the link itself, to copy again. `linkLine_build` (`app/link.ts`) draws the address in full behind a LINK pill; a press on either copies, and the pill reads COPIED, or SELECT + COPY with the address selected where the browser has no clipboard (an http page), for 2.5 s. FILES carries one under every trail; a row's LINK points that line at the row and copies it there (and notes it on the console, as before). RUNS carries one in the graph's top corner that follows the selection (the node, else the feed), and the rail's LINK pill is gone. Verified live on an isolated daemon at 2560×1440: RUNS on node 639204 showed its link and the press answered SELECT + COPY (headless over http has no clipboard); FILES at the node's folder showed both panes' links. Law a-link-is-in-sight. | #1073; `app/link.ts`, `features/files/panel.ts`, `features/dag/panel.ts`, `app/browser.ts`, `index.html`, `lcars/argus.css`
| 2026-10-15 (a census blooms with the wave) | The operator compared one feed on two devices: the tablet's nodes bloomed as the wave arrived, the desktop's were faceted stones with no bloom. Same build, different kept modes: the desktop had CENSUS on. A census drew each member as a detail-1 icosahedron (80 faces) and, being instanced members rather than one sphere per stage, never received the lamps a stage's sphere wears. Now a member's cut scales with the crowd (`censusDetail_derive`: 980 faces to 2,000 members, 320 to 20,000, 80 past), and each census stage carries a glow that blooms as the wave lands on it (none past `CENSUS_GLOW_CAP`, 400 stages, where each glow would be a draw of its own). The operator's decision: PULSE stands on by default. Every press keeps every mode, so a device's record said `pulse: false` without anyone choosing it; the record now carries a version, and one from before reads the new default for pulse alone. Verified on feed 4389 with the desktop's kept modes at 2560×1440: smooth spheres, stages blooming down the graph frame by frame. | #1070; `orrery/src/draw/censusField.ts`, `features/feed/view.ts`
| 2026-10-14 (columns give way) | At the operator's scale (about 1333×833 at 1.5×) RUNS' eleven columns needed 1,470 px in a 1,069 px field; the field's `overflow-x: hidden` clipped the last 400 px under the frame's right rail, so CREATED read CREA and the frame's inner corner sat over the first row's date — the black square at the right of listings. A trait may now declare `shed` (a rank): the listing fits its columns on every resize, from all of them, setting aside the lowest-ranked until the caps row fits; a set-aside column leaves the template and its cap and cells go with it, so the rest stay aligned. The bar says `n COLUMNS HIDDEN`, the COLUMNS block reads `COLUMNS · n HIDDEN` and names them on hover, and a wider pane brings them back. RUNS gives way NODES, TIME, SIZE, OWNER, PROGRESS (ID, TITLE, STATUS, CREATED stay); FILES OWNER, TYPE, DATE (NAME, SIZE stay); PACS patients SERVER, SERIES, STUDIES. Law a-row-never-runs-under-the-rail. | #1067; `features/roster/listing.ts` (`shed_fit`, Level `shed_set`), `features/roster/row.ts` (`shed`), `features/dag/roster.ts`, `features/files/panel.ts`, `features/pacs/panel.ts`
| 2026-10-14 (a share is news until it is seen) | The operator logged into titan after Sandip shared feed 4389 and RUNS gave no sign of it: the session's boot walk found the share before the browser attached, its arrival pulse lasted a minute, and the feed (made in August) sat at row 34 of 730 in date order. Found, never announced. A feed another identity owns, not public, that arrives in the roster is now marked NEW since the last visit and the mark is kept with the roster (`unseen` in roster.json), so it survives restarts and logins. RUNS leads with a NEW SINCE YOUR LAST VISIT block above the roster in date order, each row marked `NEW · FROM <owner>`, the bar reads `n NEW SINCE YOUR LAST VISIT`, opening a feed takes its mark off and CLEAR NEW on the frame takes them all; `proc feeds --new` lists them and `proc seen <feed …> \| all` clears. A feed this identity made, or a public one, is never marked. Row counters run on across a field's blocks. | #1065; cumin `ProcCache.unseen_*`, `procCheckpoint` roster `unseen`; salsa `arrivalsUnseen_mark`; brasa `proc feeds --new`, `proc seen`; `features/dag/panel.ts` (`chooser_show`, `unseen_clear`), `features/roster/listing.ts` (`listingPills_paint`)
| 2026-10-14 (a link opens where it points) | Handing a colleague a node meant pasting a long `pwd` into chat (#1061). A link is the door's address with a path as its fragment: `url [path]` in chell prints it (the door from `CHELL_DOOR`; without one, the fragment and a word saying so), LINK on a RUNS graph copies one for the selected node or the feed, and LINK on a FILES row for that folder or file. The fragment survives the door's login redirects, and the page that opens lands there once: a feed or a node in RUNS through JUMP TO's own landing, anything else in FILES (a file opens its folder); the fragment is then dropped. Landing never moves the session's working directory. Law a-link-lands-where-it-points. | #1061; brasa `builtins/fs/url.ts`, `app/link.ts` (`linkLanding_parse`, `link_compose`), `features/dag/panel.ts` (`link_copy`, `target_jumpTo`), `app/browser.ts` (FILES LINK), `features/roster/verbs.ts`
| 2026-10-14 (a jump lands where a number points) | A colleague named a node by number ("look at 639204") and the operator had to find its feed by hand (#1061). `proc jump <target>` takes a feed id, a node id, or a `/proc` name (`feed_4389`, `pl-dicom_repack_639204`, `_639204`) and goes there, asking CUBE for what the roster and the index lack — a node in a feed shared since the walk, its feed adopted on the way. A bare number that is both a feed and a node is never guessed: chell names both with the line that reaches each and exits 1; RUNS offers both as a choice (`FEED 4389 · 3d-anon`, `NODE 4389 · pl-dircopy IN FEED 12`). RUNS gains JUMP TO on the roster frame: the pane asks for the number, runs `proc jump --where` (which moves nothing: the console's cwd is the operator's), enters the feed and selects the node once its graph is drawn (the orrery's new `selection_set`). Nothing visible is said on the bar: not shared with you, or shared to a group you are not in. | #1061; salsa `procTarget_locate`, brasa `proc jump`, menu `proc.jump`, orrery `selection_set`, `features/dag/panel.ts` (`jump_ask`, `jump_take`)
| 2026-10-14 (a share is seen on the next look) | A feed shared to this identity in the morning was absent from RUNS after login, and `proc feeds 4389` answered "No feeds found with title containing 4389" (#1058). The roster's delta walk asked CUBE only for feeds newer than the highest known id; a share carries an old id, so only the ten-minute full walk could place it, off the lane, and the search read titles only. Now the delta reads each collection's newest page unfiltered (CUBE orders feeds newest first) and holds its total against the last walk's: a moved total — a share, an unshare, a deletion — promotes the visit to the full walk, for the same two wire calls. `proc feeds <q>` matches title, `feed_N` name and id, and a bare id the roster lacks is asked of CUBE live: adopted and listed with a line saying so, or refused with why ("not shared with you, or shared to a group you are not in"); `ls /proc/jobs/feed_N` asks the same way. The RUNS roster's filter does what the console does: a filter naming a feed by id that matches nothing asks the session, the row arrives marked as an arrival, or the field carries CUBE's reason (the listing's new `filteredEmpty` declaration). The operator asked what userB does when the roster paints without the share: wait, for how long? So the roster frame gains REFRESH (`proc feeds --fresh`: the whole index walked now, off the lane) and the bar says what the session is doing — `CHECKING CUBE FOR THE FEED LIST…` while any full walk runs, pressed or automatic, then `FEED LIST CURRENT · n NEW` or `· NOTHING NEW` for five seconds. Law a-share-is-seen-on-the-next-look. | #1058; salsa `vfs/providers/proc.ts` (`procRosterDelta_run`, `procFeed_probe`), brasa `builtins/proc.ts`, cumin `feeds_find`; `features/dag/panel.ts` (`filterMiss_note`, `rosterCheck_observe`), `features/roster/listing.ts`; smoke runs-filter-asks, runs-refresh
| 2026-10-10 (a session wears its composition's brand) | The frame-alone session still wore ChRIS's marks (#985): the brand logo in the header, the credit line's "a ChRIS system interface", the brain splash, the favicon and cumin, salsa and chili in the version stack. Each is now the composition's. The brand mark and the credit clause carry `data-composition="chris"`; the version stack names the ChRIS packages only for a ChRIS session; a session over any other backend draws an empty splash and one console line naming the backend (`mise · a session over the null backend: its files, the console, the frame`) and takes the frame's neutral favicon. A ChRIS session is unchanged. Proven against `calypso --backend null` and ChRIS by the smoke stages frame-alone, landing, help-pane and about, and by screenshots.
| 2026-10-10 (an unknown backend gets the frame alone) | A2's last step (#985, the operator's choice of a pre-boot question, 2026-10-10): before it builds, ARGUS asks the daemon `GET /backend` (`backend_ask`; token-gated, porter adds the token behind a door) and draws ChRIS's composition only for `chris` (an absent answer, an older daemon, is ChRIS). Any other backend gets the frame alone: its five tiles (FILES, PANES, KEYS, WHAT'S NEW, CONSOLE; FILES says what any home holds, the composition what its home holds), files over the session's store, the console and its own words. ChRIS's objects are still built, so the frame's closures stay whole, but none is registered: no routes, panes, presets, tiles, words or instruments, and no ChRIS question asked after attach (the /bin seed, the cohort). What the composition owns in the page's markup (RUNS and PACS in the gutter and the empty pane, the PACS workspace, the GATHER pill, the index instrument) carries `data-composition="chris"` and stands only under it; the body's `data-composition` names the session's. A saved preset the composition has not got (RUNS) falls back to home. Proven against `calypso --backend null` by the smoke stage `frame-alone`, desk and phone screenshots.
| 2026-10-10 (the composition hears its signals) | A2 step seven (#985): the index instrument (jobs and feeds, the lab's pulse, CUBE's pace, the ERRORED press into the roster) and the ChRIS panes' share of the session's prompt context, telemetry heartbeat and progress (RUNS, the universe, PACS) move to `compositions/chris/signals.ts` (`chrisSignals_make`). The frame keeps what any session has: the terminal's prompt, the status bar, the lane and beat, the restart control, the cascade. The prompt segments and the status bar's JOBS readout draw only from fields ChRIS's daemon sends (a PACS server, the physical mode, the index warm-up), so a session without them draws none. Nothing on screen changes.
| 2026-10-10 (the language takes a composition's words) | A2 step six (#985): the ARGUS language holds the frame's subjects (pane, view, file, header, console, back, desktop, dashboard, help, notes, attach, argus) and takes a composition's through `language_extend`: its own subjects and the verbs it claims from the session's (law surface-never-shadows-the-session reads them from `compositions/*/lang.ts`), the views, claims and listings it adds to the frame's verbs, its drawer chords placed after the chord they name, and its verb lines after the line they name. `compositions/chris/lang.ts` installs runs, dag, node, universe, pacs (sort, filter), image and tags, chords 2 and 3, and the ChRIS host verbs. With ChRIS installed the verb table and the chord table read exactly as before (a test holds them to the old literal tables); `keys.adoc` is generated from both sources and is unchanged.
| 2026-10-10 (the dashboard's ChRIS tiles are the composition's) | A2 step five (#985): ANALYSES, PACS and the UNIVERSE leave `app/dashboardTiles.ts` for `compositions/chris/tiles.ts` (`chrisTiles_make`). The frame keeps FILES, PANES, KEYS, WHAT'S NEW and CONSOLE, and places a composition's `TileContribution`: a lead tile that takes the wide seat when its weight outruns home's entries, tiles after FILES, tiles after PANES. With no composition FILES leads. The order on a ChRIS session is unchanged (ANALYSES, FILES, PACS, PANES, UNIVERSE, KEYS, WHAT'S NEW, CONSOLE). The gutter's ChRIS buttons go with the frame-alone step.
| 2026-10-10 (the stage is the composition's) | A2 step four (#985): `compositions/chris/stage.ts` declares what ChRIS puts on the frame's stage — RUNS, the UNIVERSE and PACS built once and raised whole, their full-workspace presets, the domains that never go dormant (RUNS and PACS), and the panes it splits in (RUNS, the universe, DICOM images and tags, GATHER). `main.ts` adopts, mounts, chromes and registers whatever the stage names; the primaries are registered (`primary_register`) beside the frame's own files rather than fixed. Home with RUNS beside it stays with RUNS's summon, which moves with the gutter. Nothing on screen changes.
| 2026-10-10 (; returns to the pane you split from) | The nightlies turned prefix-chords-2 red from 2026-10-09 (#1049): after a split, Ctrl-B ; (tmux's last pane) did not return to the pane split from, and Space then flipped the wrong pair. The layout gave the new pane focus directly, never recording the one that had it, so `;` found nothing to return to unless something earlier had happened to record it. A split and a move now keep the pane that had focus as the last pane (`focus_move`). The same scenario's break-out check counted PANES cards, which cannot grow at the cap or when the same arrangement is carded again; it now asks for a card that left the stage at the break-out.
| 2026-10-10 (DICOM is the composition's too) | A2 step three (#985): the image and tags panes and the verbs that open them (`image_open`, `imagePane_resolve`, `series_ask`, `tags_ask`, `tagsPane_open`, `tagsPane_follow`, `subfolders_ask`) leave `main.ts` for `compositions/chris/dicom.ts`, each a function of its own taking the surface as a context, bound by `chrisDicom_make`. Nothing on screen changes; every image scenario passes as before.
| 2026-10-10 (RUNS and the universe are the composition's panes) | A2 step two (#985): the RUNS and UNIVERSE pane builders leave `main.ts` for `compositions/chris/panes.ts`, made by `chrisPanes_make` from a context whose every member is read when used (the client exists only after attach). The frame's DOM helpers (`element_require`, `template_stamp`, `pane_find`) move to `frame/dom.ts`, shared by frame and composition. Nothing on screen changes; the DICOM panes and the primaries and presets follow.
| 2026-10-10 (a model finds its pane through the composition) | The frame's own model table (#985, A2 step one): `frame/modelRouter.ts` holds every route from a result's model kind to the pane that shows it, per channel (console results, ambient refreshes), with observers, claims (the pane kind a model makes an empty pane) and watch listeners. The frame routes only what it owns (a listing claims files; the cwd-following browsers; a verb that changed a folder re-lists it). Everything ChRIS's — a DAG to the focused RUNS pane, an image intent, the sampler's DAG refresh into RUNS and the universe, PACS seeing the rest, watch states, the dag, pacs, image and tags claims — is installed by `compositions/chris/routes.ts`, so `main.ts` and the empty pane name no ChRIS pane. Behaviour is unchanged: the same order, the same panes.
| 2026-10-10 (a session says what it runs over) | Step one of splitting ARGUS into a frame and compositions (#985, agreed with the operator 2026-10-10): the daemon's attach answer names the session's backend (`backend: 'chris'`, `'null'`), and ARGUS reads it through `attachBackend_get`. A daemon older than the field is ChRIS, so a new ARGUS against an old calypso draws exactly what it drew. Nothing on screen changes in this step; the next one gives each backend id its composition and an unknown id the frame alone.
| 2026-10-08 (the keys reach the frame) | The operator asked whether a workflow could be done by keyboard alone: log in, open PACS, search an MRN, view the image, change the render, close every pane. Everything but the middle could, by typed verbs; but no key opened a pane's frame, a listing's rows could not be reached at all, and closing every pane took a chord per pane. Three gaps, closed together (law a-frame-is-reachable-by-keys): a row cursor and Enter over any listing, the keys into the frame (→ or Ctrl-B r), and Ctrl-B & with `pane close all`. Live on PACS: the command line ran the query, Ctrl-B o reached the PACS pane, End and Enter indicated a study, Enter again folded its series open, a series row's PULL and GATHER rode the frame and → put the keys on them. Two seams the run found: a frame opening from its strip redraws its verbs and dropped the focus given too early (it is now given until it holds), and a study's go is its control cell, not a double-click.
| 2026-10-07 (threads carry the wave too) | The operator, on the radstar universe on titan: under CENSUS the galaxy's threads carried the pulse, but in SHAPE, still stars, only the tubes near the camera pulsed and the far connections were still threads that never moved. The stars' threads now draw with the census lines' wave material (one `lineMaterial_make`): replay into a finished stage at its depth, stream into a live one, rest for faint scenery; the fades a replay and a molecule's hand-off write dim the wave with the thread, and a hidden thread carries none. The operator also saw long straight tendrils leaving the galaxies under CENSUS and suspected a logic error; on this identity's universe no edge is out of line (median 18, longest 86, a 23-stage feed), so the scene's state now names the median edge and the longest with its two stages (`edgeMedian`, `edgeLongest` in `universe state`) to settle it on the universe that shows them.
| 2026-10-07 (a smoke's browser goes with it) | The operator: pangea's load was 150. Nineteen headless chromiums were left behind by smoke runs and probes whose script threw or timed out before it closed its browser, each still rendering at a core or more, some for eleven days. The smoke driver now starts its browser as its own process group and ties it to the script: a normal end, a thrown error, an unhandled rejection, Ctrl-C, a `timeout`'s SIGTERM or a closed terminal ends the whole group, and a browser that never opens its port is ended before the error. A browser already answering on the driver's port is refused by name rather than driven in place of a fresh one. Proved on all five endings (none left), against main's driver as the control (a thrown error left one).
| 2026-10-07 (the wave runs down a line) | The operator, on feed 2583 in CENSUS: SHAPE's spheres sit in pulsing tubes, but CENSUS with SPHERES drew thin connectors, no tubes, and no pulse. By design a census draws tubes only up to 20,000 job-to-job connections (a lab's whole history in cylinders would stall a tablet), and past that, or under STARS, plain lines that never moved. The operator chose to keep the lines and pulse them: "The effect of information traveling down a line is quite powerful." orrery's lines now carry the tubes' own wave (one shared shader function: stream into a live stage, replay a finished run stage by stage, rest, repeat), on the tubes' clock, at any size. orrery's state says how many lines the census drew (`censusLines`).
| 2026-10-07 (CENSUS sees every job) | The operator, on feed 2583 in RUNS after the feed view shipped: CENSUS "should show ALL 80k spheres but it clearly shows a tiny fraction -- in fact CENSUS seems pretty much the same as SHAPE". The one builder had marked every feed node solid under SPHERES, and orrery's census shells every job but the solid ones (solid was how an entered feed stayed spheres while the space around it was a census), so a feed view's census had nothing left to shell. Feed nodes are now tubed and never solid: SPHERES draws them lit anyway, and the census shells them all, from either door. orrery publishes the census's member count (`data-census` on the canvas, `censusMembers` in the state), and smoke feed-one-view requires it in RUNS; the universe stage requires the entered feed's census to hold a member per node and no node held out.
| 2026-10-06 (a feed has one view) | The operator, looking at RUNS after the universe gained tubes, stars and frame options: the two renderings must share one source of truth. A survey found one engine (orrery) under both and the split one layer up: two builders for the one `feed.dag`, two frames, two draw defaults, and RUNS still drawing a private older universe. Grilled into epic #943; slice 1 gave both doors one builder and made the descent land on the RANKED tree, root on top, as the roster does (the operator: "users are used to the tree having the start at top"); slice 2 gave them one frame and one per-device memory. Found on the way: STARS was a dead pill inside a feed, because every feed node was marked solid for its tube and solid nodes draw as spheres even under stars — orrery now separates `tubed` from `solid`, and a star-drawn feed is threaded finely. Slice 5's both-doors scenario then found the last seam: OPEN FEED left the universe still inside the feed off stage, so a mode turned in RUNS was not there when the universe came back — it now takes the device's memory again whenever it returns to the stage (`stage_returned`).
| 2026-10-06 (a boundary follows the hand) | The operator, dragging a files listing's columns: the NAME | TYPE grip showed a resize cursor and did nothing, and every other grip grew its column leftwards, away from the hand — "completely and totally counterintuitive. How could you even pattern match this as a viable solution?" The grip had been built as "size the column to the grip's left, the expanse absorbs", which only feels right when the expanse is on the right (TITLE | PROGRESS, the roster it was tried on). A file manager's grip moves the boundary under it; the two columns beside it trade. Rebuilt that way (`boundary_drag`, unit-tested on every side combination), measured on tracks not cap boxes (a cap's 3 px margin made every boundary overshoot), with a pinned expanse moved by its minimum and no boundary ever moving against the hand; a smoke check now drags every grip of a files listing and asks that its boundary land under the pointer, at the desk and on a tablet under a finger. The lesson: a gesture is verified on the listing whose layout is least like the one it was built on.
| 2026-10-06 (chell auth) | The operator asked how a host script reaches ChRIS through a door without a password per call, named the practice to hold to first, and grilled five decisions; then: "the name of the game here is ease of use" — users know `gh auth` and `claude login`. The design moved from a `--door … login` flag salad to `chell auth login | status | logout | token | tokens` with a remembered default door and a device-code login by default (the browser on any device; the password never crosses the terminal), with the password prompt and a pasted token as the ladder down, and `CHELL_DOOR_TOKEN` for runners. Built as #925 S1–S5 in a day (porter store and routes, porter CLI, chell auth, the default door, docs and the nightly). Two things found only live: two logins during a boot restarted each other's boot, and port 4190 is on the fetch spec's blocked list. Laws a-secret-is-never-on-the-command-line, a-token-dies-on-its-day.
| 2026-10-06 (a race named by running it three times at once) | The first nightlies turned four scenarios red (`question-both-places`, the edit pane's REVERT and Ctrl-S, two hygiene checks), every one green alone, even on a freshly booted daemon. Four hours of bisecting by stage found no stage: every subset of the suite failed and no pair did. The step diagnostics then said the scenario's `cd` had not landed when the pane was asked, and the same three-scenario run started three times at once gave a crash, a red and a green. The scenario typed lines with fixed two-second sleeps and never waited for the prompt; under load the lines queued behind a busy console the surface never declared. The lessons: a race is not found by bisecting stages, it is found by changing the load; and a state the harness has to guess at (busy) is a state the surface failed to declare. Law a-scenario-runs-a-line-to-its-prompt; the console wears `data-busy`.
| 2026-10-05 (one torn shard) | The login restart (#911) brought the operator's titan session up with a cold /proc index, rebuilding 729 feeds from scratch over a 736-shard checkpoint that was whole on disk: "I've got a cold proc state! It's rebuilding from scratch! wtf". One shard held 475 of its feed's 477 instances, two naming a parent the walk never saw, and the restore validated the assembled snapshot whole, so one torn feed refused 210,010 instances, against the module's own words (one bad file costs one feed). The boot then read like a first boot. The lessons: a validator that quarantines must quarantine at the grain it promises, and a refusal the operator pays for in minutes is said with its reason the moment it happens, never left to be inferred from a slow readout. Fixed for #917: the shard is left out and named, its feed walks again, the Jobs row says `Checkpoint refused (…)` or `N feeds with a torn shard walk again`.
| 2026-09-25 (a dropped slice is asked again) | The operator, on a phone over a cell network: "I got refused 179 (out of 192). I'm guessing there should be a reload button in frame or at least some mitigating for lost slices?" The pane asked for every slice of a series at once and never again for one that failed; through an HTTP/2 door a phone really did send 192 requests together, and a weak link dropped most of them for good. Now slices are asked for eight at a time; each refused one is asked again three times with a growing pause (0.5, 1.5, 4 s); one with no answer in 30 s counts as refused rather than holding its place for ever; and whatever is still refused puts RETRY n on the frame, which asks again for just those. Measured with the network cut mid-load: a 20 s outage left 16 refused, one press of RETRY brought all 16; a 3 s outage healed on its own. Eight at a time is not slower — the same 192-slice series filled in 113 s against 165 s when all were asked at once. Found on the way, not fixed here: a slice through the session's /vfs route takes about 5 s, so a series takes minutes; worth its own look. |
| 2026-09-24 (a census wears the tubes) | The operator: "the census display doesn't seem to have the cylinders? The pulse is actually really cool." A census draws every job of a `×N` stage as its own sphere and joined each job to the job of its parent stage with a one-pixel line; the tubes and their pulse were built for solid spheres only. Drawn as spheres (the DAG pane's CENSUS, the universe's SPHERES), a census now joins its jobs with the same tubes — sized to the jobs, red into a failed stage, streaming into a running one, and a finished run replaying stage by stage with every job of a stage pulsing together, so a fan of hundreds goes as one. Up to 20,000 job-to-job edges; past that a census keeps its lines (a lab's whole history is hundreds of thousands, too many cylinders for the tablet it may be read on), and `universe state` says which it drew (`censusTubes=`). Drawn as stars a census keeps its faint threads, as a feed does before it is entered. Verified on a 5-branch fan feed: tubes from every job to its parent's, the failed branch red, pulses running down the fans. |
| 2026-09-24 (a click takes the star you read) | Two reports the operator made on the same day, each verified on their own 4K screen and refuted by the tests that had passed. (1) DICOMs never rendered — "the whole point of ChRIS is to render DICOMs"; the pane sat at LOADING 1 OF 192. Reproduced on the live session, straight to the daemon: every slice failed with `Failed to execute 'open' on 'XMLHttpRequest': Invalid URL`. The image engines glued `location.origin` to the byte URL, which since the surface went under a prefix (porter's `/s/<key>/`, 09-21) answers relative — `http://host:4180vfs?path=…`, no slash. Every DICOM and NIfTI view had been dead since, and the image smoke scenarios that would have caught it were never in the per-slice set. Fixed by one resolution (`byteUrl_absolute`, against `location.href`, prefix kept), pinned by unit tests at the root and behind a door, and the image scenarios now run with any change that touches argus. (2) A click on a star "zooms out, pauses, and the feed barely appears", with no cylinders. Reproduced at 4K with a real double click: the tip named feed 3331 and the click entered 3494 — the space kept turning under the pointer, so a click landed on a neighbour; the first flight framed the whole feed, which in a galaxy is a long filament, so it pulled back; the entered feed was framed loosely; and the tubes were a sixth of a node's radius, hairlines at that framing. Now the space holds still while the pointer is over it; a click takes the node the tip names while the pointer has not moved off it; the first flight dollies toward the star clicked (`camera_flyToward`); the feed is framed whole with a small margin; tubes are a third of a node's radius. Verified the same way the operator checks: 4K, real double click, frames sent — the tip and the entered feed agree, the camera flies in, the feed fills the view in cylinders with its wave. And still the operator saw no cylinders and a resetting camera: the difference was in their browser, not the build. A new verb, `universe state`, lets the pane say what it holds (draw, layout, kept settings, how many nodes are marked solid and how many are drawn solid); their answer read `census=true … solidMarked=12 solidDrawn=0`. Their kept DENSITY was CENSUS, and the census drew every node — the entered feed's too — as its points, and re-parked the camera on every rebuild, the descent's included. Now the census shells everything but the solid nodes, which stand as spheres in tubes; it parks the camera only when a framing is asked for; and a solid sphere wins the pick over a census member. A smoke check enters a feed under CENSUS and reads `universe state`. The lesson for the test that passed: it ran in the default state, and the operator's browser was not in it. |
| 2026-09-23 (the universe settles in a worker) | The operator, on a second identity with 2,435 feeds: the settle "persistently only gets to 98%", "massive delays", and zoomed into a feed "I don't see the feed connection edges" — then: "how about thin cylinders connecting nodes when close … pulses along direction of signal movement", and, of the new look, "have spokes as a frame rendering option". Checked on the live session (attached read-only with the operator's leave) and on copies of it: the settle ran 26,402 spheres against each other for 150 ticks — 152 s — and did finish; "stuck at 98 %" was the readout never leaving the screen, a stylesheet's `display: flex` outranking the browser's `[hidden]`, so it stood at its last figure from the first settle on (a probe reading the flag missed it; the smoke now reads the computed display). A hidden tab also froze the settle, which ran on animation frames. Fixed at the root: the universe settles as a HIERARCHY in a WORKER — each molecule alone (one layout per distinct shape, reused and turned at random for each copy: a lab's feeds are mostly a few pipelines), then the molecules as bodies with their shapes' hubs — about 6 s for that space instead of 152, off the page, running in a hidden tab. Three arrangements on a LAYOUT block. The first two built on the hierarchy were constructed pictures, and the operator named what they had lost: "I do like the emergent shape approach … especially in radstar it was clear there were two primary types of pipelines mostly used and that spontaneously emerged." So GALAXY is the default: the flat settle, every sphere against every other with the springs holding, in the worker, started from the spokes arrangement so the same forces refine instead of unfolding from a heap (radstar's space in about 90 s on a first open instead of 152, the page live throughout). SPOKES (each feed's root just outside its hub, its chain pointing out, the reach growing with the crowd; 0.3 s) and CLUMPS (feeds packed round the hub; 6 s) stay one press away. Each arrangement keeps its own positions — in the session and per identity — so a return to one already seen redraws at once: GALAXY to CLUMPS and back measured 6.1 s then 1.9 s, against 6 s then 90 s before. Up close, edges are TUBES: lit, red into a failed stage, and carrying pulses from parent to child — a tube into a running or waiting stage streams (data is moving there now), a finished feed fires ONE wave in run order as it turns solid or is entered, then rests; the pulse means what the DAG pane's does, history-honest. The hand-off now reads a feed's nearest sphere, and a star never swells past 28 px, so a sprawling feed near the camera turns solid instead of leaving one sphere a blinding glow. The same space arriving twice (two asks crossed) is taken once, and the universe frames its bulk. Measured on the GPU on a copy of that session: ASKING 1.8 s, 60 fps; smoke 21/21. |
| 2026-09-23 (a near feed turns solid) | Slice 2 of the star rendering, the hand-off (plan: universe-stars). Under STARS a feed turns solid as a whole when its largest sphere spans more than seven pixels on screen, and back to stars below five — the gap keeps a feed at the edge from flickering — crossfading over 300 ms: its stars fade as its spheres rise, its threads fade as its own edges rise. Its spheres and edges exist only while it is solid, built when the fade begins and let go when it ends, so the cost of solidity follows the camera: detail where the operator looks, stars everywhere else — the whole-slide viewer's rule. A feed is the unit (the host names it, `SceneHandlers.handoffKey`; a folded shape's molecule is one too) so a molecule never shows half star, half sphere. A solid sphere is picked as a sphere; its star no longer is. The dimmed field behind an entered feed never turns solid (scenery), and an entered feed is solid from the start. Measured on the GPU, zooming from the whole space into its core: 16.7 ms a frame at every stage, the nearest feeds solid and shaded, the rest stars; a hover on a solid node named its feed, a click entered it. Seen and left for the operator's eye: a cluster's nebula near the camera is a large soft glow — nebulae do not hand off. |
| 2026-09-23 (the universe is stars) | Slice 1 of the star rendering, grilled with the operator (plan: universe-stars). The operator, of a login running mostly two pipelines: "two distinct galaxies orbiting their centers … rendering the spheres as something less intense at that level could be cool." Every sphere kept full geometry at every distance — about 860 triangles, its own mesh and lit material — so 3,034 spheres were some 2.6 million triangles and three thousand draw calls a frame, plus a line object per edge, shading specks a pixel wide. Settled: a STARS / SPHERES block on the frame, STARS by default. Under STARS every node is a point of light in one batch, drawn at its sphere's true size with a two-pixel floor so no feed vanishes; error-free stars glow additively (a galaxy's core brightens where they crowd), and stars with any errored share are EMBERS drawn after them in plain blending, hued as the sphere is, so failure shows through the light — the law that one datum must not erase the rest, applied to blending. Edges are faint threads in one batch; a cluster's halo becomes a nebula glow at its anchor, still its handle (hover names the shape, a click lights it; a star under the pointer wins, as a solid sphere beats a halo). A feed the operator enters is always solid (`SceneNode.solid`), the dimmed field behind it dimmed stars; CENSUS under STARS draws its members as stars. Picking is exact in screen space (a star within its drawn size, the nearest centre winning; one matrix and no allocation per star, since a census holds tens of thousands). The frame's four choices — draw, view, scale, density — are kept per identity beside the positions; the physics knobs, being play, are not. Measured on the GPU, same page, same space: SPHERES 50 ms a frame (20 fps), STARS 16.7 ms (60 fps, vsync-bound), CENSUS as stars 16.7 ms. Found and fixed on the way: a settle in slices overtaken by a quick rebuild left its readout at 99 %; an overtaken settle now closes it, and the pane no longer restarts a settle when the identity's arrival recalls nothing new. Slice 2 is the hand-off: a feed larger than about six pixels on screen turns solid, crossfading, its threads rising to edges — up close a census's nearest stars swell into glows until then. |
| 2026-09-23 (a stale page says so) | The operator, after argus was rebuilt under an open tab: "something seems to have broken in cornerstone or how DICOMs are processed" — the developer console showed `Failed to fetch dynamically imported module: …/assets/cornerstoneEngine-bNIs8OPH.js` (404). Nothing about DICOM was broken. argus loads its image engines on demand as chunks whose names carry the build's hash; a tab loaded before a deploy holds the old names, and the server now has only the new ones. Any deploy does this to an open page — an `npm install -g` upgrade on titan, a `make cook` on pangea — and the page said nothing: an image pane simply never opened. Under honest-wait (a failure is visible and truthful), the page now watches for it (`app/stalePage.ts`): Vite's loader raises `vite:preloadError` for a failed chunk, and a native `unhandledrejection` carrying the browsers' words for a failed module load is the fallback. Once — however many chunks fail — the console says `argus: the server has a newer argus than this page — reload to continue`, and a notice stands over the stage, `ARGUS WAS UPDATED ON THE SERVER — THIS PAGE IS OLDER`, carrying the control that answers it, RELOAD. Proven in Chrome by a real failed module load, and by the smoke `stale-page`. |
| 2026-09-23 (a wait shows its progress) | The operator, logged in again as a second identity with a whole index: "In dashboard I chose universe. 2435 feeds and 36 shapes are reported … still there was about a 10s delay. I had no idea if something was loading, if something was dead … Nada … AEGIS should have a rule that any UI change/update that requires more than 2s needs a progress bar." Adopted as the law a-wait-over-two-seconds-shows-progress. Measured on the GPU before changing anything: opening the universe held the page 15.7 s — the force settle of 3,034 spheres ran as one call (13.6 s, nearly all of it the charge's tree walk), so nothing, not even a bar, could have been drawn, and the title said `0 FEEDS · INDEX WARMING` the whole time over an index that was whole. Three changes. (1) A settle too big for one frame runs in slices of about 12 ms, one per frame, and says how far it has come (`SceneHandlers.progress`); the universe shows `SETTLING 3,034 SPHERES · 43%` on a bar over the field (`WaitProgress`, `features/wait/progress.ts`) while the space that stands stays drawn and live. Before the answer arrives the field says `ASKING THE SESSION FOR THE SPACE` and the title `UNIVERSE — ASKING THE SESSION …`. (2) A settle has a mode (`SettleMode`): `full` re-settles every node (a physics knob, a scale, a view); `new` holds what already stands and settles only newcomers; `hold` moves nothing when every node stands (a census toggle, a palette change). The universe paints `new` by default, so a space re-shown with remembered positions — a fresh login, a reload, the climb back out of a feed (which was a hidden eight-second settle) — appears at once, and a landing during a sweep settles only its own molecule instead of three thousand spheres a second. (3) A first paint no longer settles twice: a changed reach is set without settling the old graph (`physics_set(terms, false)`). The cost of a slice is the nodes that move, so a descent — a handful free among thousands pinned — still lands in one call for the flight that follows. Measured after: first open, ASKING at once, then the bar from 0 % to 99 % over 14 s with the page drawing throughout (worst frame 0.88 s, once); a reload with remembered positions, the space in 0.64 s. Not yet under the law, noted: the RUNS pane's warming view, a feed's first `feed diagram`, a PACS query's wait, and the image pane's first load — each needs its own readout. |
| 2026-09-23 (the way home is one press) | The operator, on the file browser: "I find myself continually wanting to go HOME, but this requires opening the frame and hitting HOME … one too many clicks", and proposed the answer: a navigation row on the listing itself, above `..`, listed as `~`. Built as they said, on the listing's own machinery. The `~` row is a second LEAD row (outside the order, never sorted, filtered, counted, selected or given verbs, exactly as `..` is), drawn as a directory row with the house glyph in its capsule and the home path dimmed beside the name the way a link says where it points; it is offered anywhere but home itself, and it keys apart from `..`, since from a directory just under home both lead to the same path. The operator also took the second proposal: the path line above the listing is now a TRAIL — every segment but the last is a press that goes there, starting at `~` under home and at `/` elsewhere — so any ancestor is one press away, not only the parent. Both lower exactly as a directory press does (a following browser runs `cd`, a rooted one walks its own history, so BACK still returns); HOME stays on the frame for a listing scrolled deep. Home is the session's `/home/<user>`, told to each browser from the prompt. A catalogue, a picker over /bin, leads with neither. Smoke: `home-trail`. Found in passing, on main too: the `select-mode` scenario fails two checks and leaves the browser in its scratch folder, which unsettles scenarios run after it, and `process-workflow` fails three. |
| 2026-09-24 (the frame meets itself) | The operator, from an unfolded phone: "The top frame has a persistent break/discontinuity", then "the persistent right edge issue" over a zoomed universe pane whose title and drawer ran off the screen. Two faults, neither new. (1) The head: the gutter's head has a floor (its first block's 160px) while the header's face is a share of the screen's height, and the header was a block that stopped where its content did — so on any screen shorter than about 1000px the band that closes the head ended above the elbow it should meet: 42px on a 1440×800 laptop, 16px on a tablet laid flat, 45px on the fold. The header is now a column whose middle grows, and the band sits at the row's foot, level with the elbow, at every height (measured 0px at six sizes). (2) The right edge: the column holding the console and the panes, `main`, and the bars all held the width of their longest line, so a feed name in a pane's title (already set to ellipsise) held the whole column 200px past the edge. They give way to the screen now (`min-width: 0`), the title ellipsises as it was meant to, and a drawer's group of verbs wraps to a second row when a narrow screen cannot hold it. |
| 2026-09-24 (the frame on a phone) | The operator, on a phone: "Can you do any fixes to the mobile layout... it's disjointed. I also noticed that often the right part is off 'screen' but only on mobile". Measured at 412px: the page did not scroll sideways — it clipped. The gutter kept its 168px (two fifths of the screen), the header kept 387px of desktop minimums (the versions' labels hold 9.5rem each, the pills their width), and the dashboard kept three columns, so FILES and UNIVERSE were cut in half. Asked how the frame should adapt, the operator chose a slim rail over a top tab strip or a minimal fix. Below 700px now: the gutter is a 56px rail reading its blocks by number (01 … 06), the elbow's sweep and the bands shrink with it, the root type steps down to 15px; the header keeps its banner and bands, and the ARGUS WEB face — AUDIO, the theme, versions, credits — folds behind a ≡ pill at the banner's end that opens it over the page; the dashboard's blocks stand one above another in a scrolling column. The desktop frame is untouched: every change sits in one media block, and the labels' words are wrapped (`.gutter-word`, `.hop-dash`) only so the rail can fold them. The block lives in `argus.css`, headed "the frame on a phone"; its measurements are the frame's own variables (`--lfw`, `--elbow-radius`, `--elbow-inner`, `--bar-height`), so the frame rescales rather than being redrawn. Rule for what comes after: no frame element takes a fixed width or a minimum without a phone value beside it. Then, asked whether the desktop had regressed and how a Galaxy Tab S11 Ultra and an open Z Fold 7 fare: the frame's geometry was measured on main and on this change at 1920, 1480×924, 924×1480 and 882×790 and is identical above 700px — and the measuring found the header box 150px past the right edge at EVERY width on main too (a flex item will not shrink below its content), so the last column of versions had always been cut. `.right-frame-top` now takes `min-width: 0` and ends at the screen's edge everywhere; between 700 and 1100px the header's readouts take two columns with narrower labels; and the dashboard's rows never go below what a block holds (`minmax(min-content, 1fr)`) — on a short screen the grid scrolls instead of crushing its blocks until the words were cut. |
| 2026-09-24 (a finger steers the space) | The operator, on a phone in mobile Vivaldi: "Can you add mobile touch events like pinch to zoom etc.... I'm thinking of the universe view", then "maybe two finger up and down in a pane to scroll through slices in a series?". The scene had only a mouse's grammar — a wheel to dolly, a right button to pan, a double click to enter, a hover to name — and the canvas let the browser take a pinch for a page zoom. Now the canvas holds its own touches (`touch-action: none`) and a finger speaks the same verbs: one finger orbits (or pulls a node it lands on), a pinch dollies exactly as the wheel does (the spread's ratio, in the wheel's own units — toward the focus, or along the eye ray in census), two fingers moving together pan, a tap selects and names what it landed on (the name stays up a moment after the finger lifts, since there is no hover to hold it), and two taps close in time and place enter as a double click does. A star is hit within a fingertip's reach, not a cursor's. A lift the browser never reported cannot leave a phantom finger: the first finger of a new touch starts clean. In the image pane two fingers up or down step through the series, as the wheel does — Cornerstone's own touch binding on the stack viewports, and on the SLAB view a sweep heard before vtk's touch handling. Verified in headless Chromium with touch emulation: a spread took the camera from 1593 to 398 and a pinch back; a tap named a star, a double tap entered its feed; two-finger swipes stepped a 192-slice series. The phone layout itself — the frame's left column and header holding desktop widths, so the right part runs off screen — is the next piece. |
| 2026-09-25 (a cap ends before the elbow) | The operator, from a phone over the PACS pane: "See the right frame? I suspect the elbow construction is being shown as the black cut out". It was: the elbow's inner radius is a black fillet cut in page over whatever lies beneath, and every framed grid keeps `--elbow-clear` free on its right for it — as padding. At 412px the PACS tracks sum to ~870px in a 320px field, so the PATIENT cap ran on through its padding and under the fillet, which cut a notch out of it. The caps now clip at their own clear line (`clip-path` on `.roster-caps`), so a cap wider than the field ends square before the elbow; nothing changes where the tracks fit. In the same pane the command line wraps, so EXPORT CSV stands under the line instead of past the pane's edge as `EXPO`. Looked at 412×915 before and after. Open: the rows' further columns still pass beneath the strip on a phone — scroll sideways, or fewer columns, is a decision for the operator. | look:412px PACS pane
| 2026-09-25 (a phone climbs out) | The operator, on a phone inside a feed: "once I have in the universe view entered a node and seeing the dag how do I esc from that, i.e. go back?", then, of pinching out: "That works". A phone has no Esc. Pinch out is now leave, as double tap is enter: when the last finger of a two-finger gesture lifts with the camera drawn back half as far again as the descent framed the feed (or the cluster), the pane climbs out one level. A smaller pinch, to see a sprawling feed whole, stays. The wheel keeps free zoom — a desk has Esc. BACK stays where it has always stood, on the mode frame, one tap on the strip away on a phone. Watched at 412 px under touch emulation: a small pinch stayed inside feed 211, a hard pinch climbed out to 709 feeds. | smoke:universe-pane
| 2026-09-25 (the lab replays) | ORRERY slice 5, grilled with the operator (decision 6): one general replay reveals a space in arrival order at its final positions, the camera untouched, and the universe's arrival is when each feed was made. The kernel now says so on every universe feed (`createdAt`); the orrery keeps a replay clock and shows a star when its node arrives, with a flash that fades, its threads once both ends have. On the surface it is a REPLAY block on the mode frame that names its state (REPLAY, PLAYING, PAUSED, REPLAYED) and a day on the bar's readout, a readout and not machinery; words for everything (`universe replay [speed] \| pause \| stop \| at <date>`), and Esc ends it, a level of its own. It plays only what it can be honest about: the whole space, feed by feed, as stars — a folded shape, a census or a solid feed would reveal something that was never one feed's arrival, so those are refused by name. Watched at ×1: the first cluster alone in March 2025, a second arriving in a white flash by July, the rest filling in to August 2026. | smoke:universe-replay
| 2026-09-25 (the sky of what it ran) | ORRERY slice 6, constellations (decisions C1–C3), on the LAYOUT block as its fourth arrangement. Every plugin is a ringed star — a star of another kind among the feeds, a thin bright circle round a small core — placed first by what runs with what, then every feed's stages pulled to the stars they ran. Tuned against the operator's own 709 feeds rather than by eye: the first cut left stages a median 172 units from their star with stars 7.5 apart (a crowd's unbounded repulsion flung them out), so a stage's repulsion now reaches only five units, the pull runs from 0.3 to 0.9 by how rarely a plugin is used, and the sky grows with the stages it holds — stages now sit a median 13 from their star, and the space reads as its two pipeline families, each a cluster round its stars, the rare plugins' rings around them. The hover names a star `pl-x · n feeds · e% errored`. Lighting a plugin's stages, framing them, and opening it in /bin follow. | smoke:universe-pane
| 2026-09-25 (a star lights what it ran) | Constellations' handles (C2). A plugin star is pressed like any node: it lights the stages that ran it across the whole sky — every other stage faint, nothing moved — and its facts say PLUGIN, FEEDS and ERRORED, with the one verb it has, OPEN IN /BIN: its newest entry opened in the browser the way every /bin entry opens, its one-node graph. A double press frames the stages it ran. The stars never dim with the rest: a faint star takes no pointer, and the next plugin must stay one press away. By word, `universe plugin <name> \| off`, refused by name outside CONSTELLATIONS or for a plugin no feed ran; `universe state` says what is lit. | smoke:universe-constellations
| 2026-09-26 (the space by what it began from) | ORRERY slice 8, DATA (decision DATA), fifth on the LAYOUT block. Every feed hangs from what it began from, as the index read it (slice 7): a hub per format (DICOM, NIfTI, MGZ, JPEG, PNG, OTHER, UNREADABLE, NOT YET READ), under DICOM a hub per modality, under that a hub per series-description group — descriptions compared by their words, not their spelling, so `SAG MPRAGE 220 FOV` and `sag_mprage` are one group, named by the description most of its feeds carry. Each hub is a nebula sized by its feeds and read without hovering: formats and modalities always carry a caption (`NIfTI · 435`), a description group when it holds five feeds or two in every hundred. Found by looking, three times: captions sized in the space were invisible at the space's scale (now one size on screen); every group labelled crowded the middle unreadable (now the threshold, and where two captions meet the bigger hub's is read); hubs drawn as bodies threaded a line to every feed they held (now anchors). A hub's tip reads its path and count; a press lights its feeds; SHAPES gives way to GALAXY as under CONSTELLATIONS. The picture fills in as the index reads — a feed not yet read hangs from NOT YET READ. | smoke:universe-data
| 2026-09-26 (words the operator can put away) | The operator, seeing DATA's captions: "Captions should be toggleable yes?" Yes: captions are words laid over a picture, and the picture is the operator's. A CAPTIONS block on the universe frame turns them off and on (CAPTIONS ON / CAPTIONS OFF, as GRAVITY reads), `universe captions on\|off` by word, kept with the other frame choices per identity; off, a hub still names itself on hover, and a caption drawn while they are off stays hidden. Captions also sit on a dark plate now, so they read over the brightest cloud on a phone. | smoke:universe-data
| 2026-09-26 (the lab as a coral) | ORRERY slice 9, accretion (decisions A1, A2), sixth on the LAYOUT block. The space grown in the order it was made: the oldest feed at the heart, each after it wandering in until it touches a feed already grown and sticks — to one that ran the same plugins more readily than to a stranger — so like grows on like and the oldest work lies deepest. Measured before it was shown: the first cut unfolded each feed with the DAG pane's settle, fourteen units wide, and the space drew as spokes of single molecules; a feed now unfolds compact at the universe's own edge lengths. A feed that lands later accretes as the newest and nothing moves; `universe regrow` grows the coral afresh (gaps left by removed feeds close), and is refused by name under any other layout. REPLAY over it is the coral growing. | smoke:universe-accretion
| 2026-09-26 (each layout its own places) | Found by laying the screenshots of every layout side by side for the projections document: GALAXY, SPOKES, CLUMPS and CONSTELLATIONS were one picture. A switch filed the space as it stood under the layout it was becoming, not the one it had been, so each layout recalled the one before it and drew it held. Introduced when DATA taught the kept positions to follow the panel's layout rather than the scene's (the scene's still names the old one at that moment); never published. The space is now filed before the name changes. | smoke:universe-layouts
| 2026-09-26 (placed once, drawn everywhere) | ORRERY slice 10 (#722), as the operator chose it: the universe laid out once per identity, not once per browser. The first browser to settle a layout gives its places to the session, which keeps them beside the index; every browser after it — another machine, a phone, a cleared cache — draws the space at once and settles only what is new since. A browser that has its own places keeps them. What the session keeps is a word away (`proc layout <name>`), and `universe state` says whether it keeps this layout. Measured: a clean browser 8.1 s to a settled space, the next 1.5 s; for the largest identity here the saving is a minute per browser. | smoke:universe-session-layout
| 2026-09-30 (a node sits on its tubes) | The operator, in a graph's 2D projection: the tubes ran across the node circles, ending inside them. 2D draws nodes as discs in the plane the tubes are centred on, so a tube's near face stood in front of the disc; and a first fix by render order did nothing, because the tubes are transparent (their shader fades and pulses) and three draws every transparent thing after every opaque one whatever its order. The disc is now transparent at full opacity, ordered after the tubes, without the depth test: it covers the tube ends as a sphere's near hemisphere does in 3D. Verified by rendering the same feed before and after on a private daemon. Law: a-node-sits-on-its-tubes.
| 2026-10-01 (the content view is the pane's field) | The operator's screenshot: a cohort manifest, pretty-printed, running off the foot of its pane, the pane clipping it — no scrollbar, the last series simply absent. The content view (`.files-content`) was a block under the header with no height of its own, so it grew past the pane. It is the pane's field now: it takes the height the header leaves (`flex: 1 1 0`, `min-height: 0`) and scrolls inside it. Verified with a thirty-series manifest: 4510 px of text in a 263 px field, scrolled to the end, the field's foot on the pane's. Smoke `files-json` asserts it. Law: a-field-scrolls-inside-its-pane.
| 2026-10-01 (the grip stands whenever a face does) | The operator: the header/body boundary would not drag — "I clicked and tried but nada" — and proposed the centred bar at the band's foot as the grab, pulsing softly. The drag was built (2026-09-29) and worked; the grip was hidden by its own rule whenever `body` had no `data-header`, and that is exactly the resting state, where the versions face shows without one. So at rest there was no grip, and the bar the operator grabbed was page chrome. The grip now hides only with the header (away) and under a zoom, and breathes at rest in opacity — a brightness swing on four pixels of bar is invisible — settling still under the hand. Verified on a fresh page: no face chosen, the grip is what a press lands on, a drag of 100 px grows the band by 100 px and back. Smoke `header-grip`. Law: a-control-that-can-act-is-visible.
| 2026-10-01 (a press is acknowledged) | The operator: OPEN, or a double-click, then seconds of nothing — "did my navigation take?" Measured: the seconds were the kernel's (seven to fourteen CUBE requests per navigation, epic #794, now 0–1), but even at 0.4 s a press that changes nothing on screen is a question. Grilled: (c) both the row and the control acknowledge; (c) the bar carries the wait and the old listing stays; same-row repeat absorbed, other-row press replaces. The façade owns it, so every listing has it and no pane declares anything: `row_activate` lights the row `listing-activating` (the indicated-row look, the rest dimmed), the row's capsule works (breathes), the bar reads `OPENING <name>` in the wait hue; the listing that lands (`rows_set`) stands it down; a kernel refusal (`activation_refuse`, the files browser hands it `cd: x: Not a directory` from the error envelope) stands it down with the words on the bar in the error hue, the parent still on stage. A press on a different row while one waits replaces the intent (the kernel's lane runs both in order; the surface ends where the last press went). Smoke `press-ack`. Law: a-press-is-acknowledged.
| 2026-10-01 (a stale page says what it is and the cure) | The operator, again unable to see DICOMs, with a console reading `Cannot destructure property 'CornerstoneEngine' of (intermediate value)` and a 404 on `cornerstoneEngine-BQy2N2sN.js` — a chunk of a build three releases old. The page was a tab opened before the day's deploys. The stale-page watcher existed (2026-09-23) and had fired, but it PREVENTED Vite's `vite:preloadError`, which makes the loader resolve the import to `undefined`: the pane then fell over destructuring nothing, a TypeError that named neither the build nor the cure, and the image pane showed no DICOM and said nothing. The event is now left to throw; the image pane catches a stale chunk on both its engine imports and reads the cure on its own field — `ARGUS WAS UPDATED ON THE SERVER · RELOAD THIS PAGE` — beside the page notice, whose words now say what it is and what to do: `THIS PAGE IS AN OLDER ARGUS THAN THE SERVER NOW HAS — RELOAD TO CONTINUE`, with RELOAD. Verified live by hiding the engine chunk under a loaded page and pressing IMAGE: the field reads the cure, the console carries both notes, the notice stands with its pill. Law: a-stale-page-says-what-it-is-and-the-cure (a failure caught anywhere is bubbled to where the operator looks, as what it is and how to fix it).
| 2026-10-01 (focus names a pane on stage) | Found by the cleanup's first slice running two smoke stages in a new order (#816): after the UNIVERSE stage and the dashboard, the HELP tile opened nothing and `pane split right` made no pane. The layout's focus still named `launcher`, a pane no longer in the tree, and every spawn from the focused pane split from a leaf that was not there. The layout now keeps one invariant on every render: focus names a pane on stage; a tree that lost the focused pane hands focus to its first leaf (the domain's primary), and the pane that left is the one `;` returns to. Its own PR, by the cleanup's rule that a cleanup PR changes no behaviour. | `app/layout.ts` (`focus_reconcile`), unit tests `layoutFocus.test.ts`, smoke `help-pane` (now walks UNIVERSE → dashboard → HELP)
| 2026-10-01 (a fact has one source) | The cleanup epic's first slice (#814), from a measured survey rather than taste: the bar's state-class list was spelled seven times in four files, the primaries were tested five ways, the side/axis/place/arrow/edge mapping was written seventeen times in four forms, the pane kind was a string switched on nineteen times in the host. Each is one source now — `app/sides.ts` (one table, with `side_derive`, `side_deriveFromPlace`, `side_findByArrow`, `side_findByFocusWord`, `splitSelector_format`), `features/roster/bar.ts` (`BarState`, `barState_set`, the one writer), `app/panes.ts` (`PaneKind`, `PRIMARIES`, `pane_isPrimary`) — and the aegis lint fails any other file that spells them again. No behaviour moved: the same smoke answers before and after. One dead comparison surfaced (`(kind as string) === 'catalogue'` inside a branch where the kind could only be files or dag) and was removed as dead, not fixed as a bug. | `app/sides.ts`, `features/roster/bar.ts`, `app/panes.ts`, `scripts/aegis-lint.mjs`
| 2026-10-01 (help lives on the stage) | The operator: the keys "need to be fully documented and moreover available in the UI itself — a HELP on the dashboard? a help keystrokes in the console?" Both, from one source. A HELP pane kind (the listing façade, two blocks: the chords, the verbs; filterable, N MORE, a drawer like any pane) opens from a HELP tile on the dashboard (figures: how many keys, how many verbs; its rows the first chords) and from the typed `help pane` (bare `help` stays the session's — surface-never-shadows-the-session — the surface claims `pane`, `keys`, `verbs`); `help keys` and `help verbs` answer in the console, as a typed question does. The written reference `apps/argus/docs/keys.adoc` is generated from `DRAWER_CHORDS` by `scripts/keys-doc.mjs` (`npm run docs:keys`), and the aegis lint fails when the file and the table disagree (law keys-are-documented-from-one-table). Each chord carries a topic now (pane, focus, binding, claim, stage, console), which groups the reference and classes the rows. | `features/help/panel.ts`, `tpl-pane-help`, `app/main.ts` (`help_open`, the HELP tile), `console/argusLang.ts` (`help`, `VERB_LINES`, topics), `scripts/keys-doc.mjs`, `docs/keys.adoc`, smoke `help-pane`
| 2026-10-01 (the scene is composed of its parts) | The cleanup epic's third slice (#814): `Orrery`, orrery's scene, was one class of sixty-five methods and forty-eight fields in an 1,800-line file — the wave, the replay, the grab, the hover tip, the settle, the worker protocol and the draw all reaching the same fields. Each is a part now, beside the scene and behind a small port that names what it reads (`scene/settle.ts`, `wave.ts`, `replayTrack.ts`, `grab.ts`, `hover.ts`, `hierarchySettle.ts`, `bodies.ts`, `drawn.ts`), each with unit tests of its own (the wave's schedule and loop, the replay's show and hide, the grab's threshold and ease home, the drawn lookups); the scene keeps its fields, its rebuild, its draw in steps, and the public face a surface mounts, unchanged. `npm run lint:orrery` now also holds every orrery function under 150 lines and every file under 1,200, with a named allow-list that is empty. No behaviour moved: the universe, census, constellations, replay, /bin and diagram smoke stages answer as before. One flake found, not fixed here: the pill-bar stage's console-growth check failed once after the universe-pane stage ran before it and passed alone. | `packages/orrery/src/scene/*`, `scripts/orrery-layers.mjs`
| 2026-10-01 (a flight is one word) | The cleanup epic's fourth slice (#814): the UNIVERSE pane spelled each of its flights as five scene calls — read where the spheres stood, seed the feed's nodes about their centre, set the graph with the rest frozen and gravity off, note what it is now inside, fly to frame the bulk — round a `flying` flag of its own that nine methods consulted and three forgot to clear on refusal paths. Those are the scene's words now (orrery `scene/flights.ts`): `descent` (approach, ask the pane what to unfold, unfold or stand down), `unfold`, `relight` (held where it stands, framed) and `frame`, with `moving()` the one flag; the pane keeps its asks and its chrome, and says what it is inside in the plan's `drawn` hook, so nothing changes order. `descend` went from 82 lines to the plan it returns. The DAG pane's reading of a feed's model into scene nodes (metric by mode, compute hue by legend) is a pure builder with tests (`features/dag/sceneGraph.ts`). No behaviour moved: the same smoke stages answer as before, and the pill-bar console-growth flake seen in S3 reproduced once more in sequence (passes alone). | `features/universe/panel.ts`, `features/dag/sceneGraph.ts`, `scene/chrisSpace.ts` (`ScenePlan`, the program overrides), orrery `scene/flights.ts`
| 2026-10-01 (the host has a context) | The cleanup epic's slice 2a (#814), the first cut into the 4,800-line host closure. Eight maps of one shape — a files, dag, universe, gather, image, tags, view and help panel each by pane id, eighty-odd touches — are one `PanelRoster` (`panels.get('dag', id)`, `panels.values('files')`, `panels.idOf('image', panel)`), so a pane filed under one kind cannot be read as another and a new kind is added in one place. The host now has a `HostContext` (layout, panels, pane instances, subjects, dormant, sound; the terminal and the client as getters, since they are made last and a module reads them when called, never at wiring), and the first module takes it: `paneVerbs_wire(context)` returns the bar note, move, flip, resize and chord-key verbs the drawer's capsules and the typed `pane …` sentences share; the host destructures them under the names its call sites already use, so nothing else moved. A module names in its `Pick` the part of the context it reads. Unit tests in jsdom for the roster and the verbs. The pattern for the slices after: a module per concern (`x_wire(context)` returning its verbs), the host assembling them. No behaviour moved: the pane, chord, bar, help and roster smoke stages answer as before. | `app/panes.ts` (`PanelRoster`), `app/hostContext.ts`, `app/paneVerbs.ts`, `app/main.ts`
| 2026-10-02 (a desktop is a module) | The cleanup epic's slice 2b (#814), the first concern cut whole out of the host closure: the desktop. Its capture (161 lines, one `for` over the stage with seven `else if` branches minting actions) is `action_build` — one pane, one action or null — and `capture`, which keys and labels the card; its replay (142 lines) is `action_replay` — one action, the pane it produced — and `restore`, which lays the shape over what came back; the births the open verbs record and the replay place they read live with them. The host hands the module its open verbs as hooks (`image_open`, `dir_open`, `process_open`, `gather_open`, `feed_open`, `pillPane_spawn`, the PACS query, the catalogue bindings, the PACS relight) and keeps every call site under its old name. The replay place is read through `desktop.replayPlace_get()` at twelve opens instead of a `let` declared eleven hundred lines below them. Tested in jsdom with a recording stage: a bare domain is not carded; content is captured with its births and shape and keyed by its content; a domain switch captures, applies, sweeps; a restore replays each open from the pane a prior action produced, with the replay place set only for the open and null after, and lays the recorded shape over the panes that came back, a leaf that produced nothing left out. One dead constant removed (`IMAGE_SUBVERBS`, declared and never read). The host is 5,766 → 5,323 lines. No behaviour moved: the desktop, launcher, cards, pin, home-trail, image, gather, split and pane smoke stages answer as before. | `app/desktop.ts`, `app/main.ts`, `tests/unit/desktop.test.ts`
| 2026-10-02 (inside a node is a module) | The cleanup epic's slice 2c (#814): the node overlay — 160 lines of the host closure: the records by pane, the browser stamped over the canvas, the click that walks, hops, exits or shows a file, the way out, Esc's two levels, BACK, and the ghost-record repair — is `app/nodeOverlay.ts` behind `nodeOverlay_wire(context, hooks)`: `open`, `close`, `back`, `escape` (true when an overlay took the key), `dispose`. The click handler is `action_take`, named, instead of a hundred-line closure inside `new FilesPanel(...)`. The host's files body, rooted listing, row verbs, imagery test, table test, byte route, file reader and preview provider are its hooks; the DAG and universe builders, the drawer's BACK, the key handler's Esc step and the pane's dispose call its verbs. The `FileText` shape it reads is exported from it. No unit test: it builds a real `FilesPanel` over the template, which no jsdom test does yet; the node-dive and node-volume smoke stages cover it live. Found on the way: the host context was declared after the first module that needed it, a temporal dead zone at wiring; it stands above every module now, the layout and the dormant groups as getters like the terminal and the client, so a module may be wired anywhere in the closure. Also a smoke fragility, not a surface defect: the image-panes scenario's restore check assumes the viewer was opened in the FILES domain; run after enter-place it was captured as another domain and the check went red. The scenario enters FILES first now. And the node-dive scenario itself had never run here: it declared `const box` twice in one eval, a SyntaxError the driver reports as `Uncaught`, and it needs `SMOKE_DAG_FEED`, which a thousand-node feed defeats (the probe clicks down the middle land on nothing). Renamed; run on a six-node feed (880) it answers 4 ok, the overlay opening, arriving at the node's filesystem, leaving by Esc and by EXIT NODE. No behaviour moved. | `app/nodeOverlay.ts`, `app/main.ts`, `tests/smoke/smoke.mjs` (image-panes, node-dive)
| 2026-10-02 (the cohort is a module) | The cleanup epic's slice 2d (#814): the session's cohort — 320 lines of the host: the GATHER pane's factory, the band's panel, the set itself, the dismissed flag, the session file and its debounced write, the restore at boot, the reveal on a gather, the stage as a pane, the open beside a host — is `app/cohort.ts` behind `cohort_wire(context, hooks)`: `instance_build`, `has`, `gather`, `open`, `stage`, `annunciate`, `restore`, and the `cohort` itself. The one-source win: the band's and the pane's `GatherPanelHandlers` were spelled twice (eleven handlers each, `feed_create` forty lines twice, word for word); they are `handlers_make(hostOf, asks, own)` once — where the work lands (the pane's own id, or a pane on stage resolved at the press), how a question is asked (on the pane, or on the band with the console noting it), and what DISMISS and a change mean for that reflection. The host's open verbs, asks, spawn, birth, replay place, relight, home and sweep are hooks read when called (several are declared further down the closure). Stage verbs tested in jsdom with a recording layout: an open splits beside the host at the replay place and records the birth, falls to the errand host and the row-after default, reuses the pane on stage, lets a refused split go; a stage splits from the errand host and retracts the band, or focuses the pane already there. No behaviour moved: the gather, process, series-parity, desktop and PACS smoke stages answer as before. | `app/cohort.ts`, `app/main.ts`, `tests/unit/cohort.test.ts`
| 2026-10-02 (a question stands in a module) | The cleanup epic's slice 2e (#814), shipped with 2d: the errand and the asks — 190 lines of the host: the errand host, the `path` errand with its bar, its abandon on Esc, the pane a pressed verb remembers so the command's own question stands where the press was, and the pane ask that falls to the console when the pane is not on stage — are `app/asks.ts` behind `asks_wire(context, hooks)`: `errandHost_find`, `errand_open`, `errand_abandon` (true when one stood), `verbLine_run`, `askingPane_take` (read once and cleared, so a later question never inherits a stale pane), `askOnPane_await`. Two `let`s the key handler and the wire's ask reached into are the module's own now; the console drawer's setter stays the host's, handed in as `console_expose`. Host 4,695 lines. No behaviour moved. | `app/asks.ts`, `app/main.ts`
| 2026-10-02 (the /bin graph is a module) | The cleanup epic's slice 2f (#814): the /bin view — 485 lines of the host: the summary highlighter, the graph shown over a files panel's content view, the pipeline and plugin readers, the dive into a node and the way back, the readout painter and its form cells, the two row builders, the diagram's mode frame — is `app/binView.ts` behind `binView_wire(context, hooks)`: `entry_show`, `dive_leave`. Its pure parts are exported and tested in jsdom: `binText_highlight`, `pipelineNodeRows_build` (selected says how much there is, immersed lists it, a form qualifies each flag by the node's selector), `pluginRows_build` (the hint from type, required and default), `facts_paint` (a form's row is a cell writing the line, a boolean a check), `diagramModes_wire`. The catalogue bindings and RUN are its two hooks. Eleven imports the host no longer read left with it. Found by the test, kept as is: the highlighter's string pattern looks for `&quot;`, which `html_escape` never writes, so a quoted string in a summary has never been marked. Host 4,213 lines. No behaviour moved: the /bin, diagram and constellations smoke stages answer as before. | `app/binView.ts`, `app/main.ts`, `tests/unit/binView.test.ts`
| 2026-10-02 (the keys are a module) | The cleanup epic's slice 2g (#814): the window's capturing keydown listener — 280 lines in one closure: Esc's thirteen levels as a run of `if … return` blocks, the chords as another, the prefix as a third — is `app/keys.ts` behind `keys_wire(context, hooks)`. Esc is a ladder now, `escape_steps`, each step a named function answering claimed, yielded (the zoom's own listener takes the press) or passed; the handler walks it and sounds on every claim but the command line's, as before. The chords are `chord_handle` (a `switch` on the key, each case closing the drawers and acting), the prefix `prefix_handle`. The command line, the errand's abandon, the /bin dive's leave, the overlay's escape, the drawers' and frames' close, the console's focus, the pane verbs, the language's host and the break-out are hooks, read when a key is pressed since several are declared further down the closure. Four imports left the host with it. Host 3,957 lines. No behaviour moved: the chord, zoom, console and focus smoke stages answer as before; select-mode's two reds (the confirm's YES capsule in the console does not remove the selected files) are red on main's own build too, the same family as #826, and are noted there. | `app/keys.ts`, `app/main.ts`
| 2026-10-02 (the browser is a module) | The cleanup epic's slice 2h (#814): the files pane as the host builds it — 530 lines of the closure: the byte routes' readers (a file's head through /vfs with the session as fallback, a pipeline's glimpse, a file's text through a silent cat whose refusal is an answer), the preview provider, the rooted listing and each rooted browser's history, which browsers follow the cwd and the control that states it, delivery over /vfs, MKDIR, the file activation (a walk by `cd` or rooted, a /bin entry, imagery, an image, a table, a text), the row verbs lowering to session lines, the selection verbs as one line each, and the pane factory — is `app/browser.ts` behind `browser_wire(context, hooks)`. The drawer's HOME, BACK and binding capsules read `rooted_walk`, `rooted_back`, `follows` and `follow_set` instead of two maps of the closure. The path readings (`imagery_is`, `nodeOf_path`, `feedOf_path`, `path_isProjection`), `ansi_strip`, `shares_read` and `TABLE_FILE_PATTERN` are exported and tested; the host's own `ansi_strip` and table pattern left with them, and twelve imports. Host 3,429 lines. No behaviour moved: the home-trail, enter-place, JSON, binding, row-verb, running-row, gather, /bin, ask, image and roster smoke stages answer as before; file-download's two reds (nothing lands in the headless driver's download directory) are red on main's own build too, filed as #830. | `app/browser.ts`, `app/main.ts`, `tests/unit/browser.test.ts`
| 2026-10-02 (the chrome is a module) | The cleanup epic's slice 2i (#814): `pane_chrome_wire`, 265 lines in one closure wiring a pane's drawer and whatever its kind adds, is `app/paneChrome.ts` behind `paneChrome_wire(context, hooks)`, returning the one-pane wirer the host calls six times for its primaries and once per spawn. Three named parts: `drawer_wire` (the handle, the binding radio, SPLIT and MOVE with the four direction capsules dimmed where the pane cannot go, every chord's keys in its capsule's title, CLOSE with the primary DAG's dismissal and the root leaf's home), `files_wire` (the CWD binding pair reading the browser's state wherever it was changed; HOME, BACK, MKDIR, REFRESH and UPLOAD on the frame, acting on where the field points) and `dag_wire` (ENTER NODE, PROCESS NODE, ENTER FEED, BACK, CLEAR DETAIL, REFRESH, with the place a regard points at read once). The pane verbs, the browser, the pill spawn, the opens and the home are hooks. Host 3,195 lines. No behaviour moved: the control-homes, chord, move, zoom, home-trail, binding, row-verb, gather and enter-place smoke stages answer as before. | `app/paneChrome.ts`, `app/main.ts`
| 2026-10-02 (the language's host is a module) | The cleanup epic's slice 2j (#814), the last cut into the host closure: the `ArgusHost` object the console language reads — 245 lines, `image_control` ninety of them as one run of `if (verb === …)` — is `app/consoleHost.ts` behind `argusHost_build(context, hooks)`: `panel_findForTarget` (the pane of a kind a target stands for: itself, the one in its link group, or the only one on stage; spelled twice before, for image and tags), `imageVerb_run`, `tagsVerb_run`, `desktop_serialize`, and the host assembled from them. The pane verbs, help, the launcher, the identity, the console zoom, the primary DAG's feed enter, the file save, the image open, the series read and the tags pane are hooks. Four imports left the host with it; a jsdom test covers the focus fallback, the target resolution and the serialization. Host 2,972 lines, from 5,816 when the epic began; every concern a module of its own behind `x_wire(context, hooks)`, the closure what remains: the pane factories, the opens, the wire's observers and the boot. No behaviour moved: the image, console, language, pill-bar, ask, help and zoom smoke stages answer as before. | `app/consoleHost.ts`, `app/main.ts`
| 2026-10-02 (a part stays a part) | The cleanup epic's slice 5a (#814). The UNIVERSE pane's `control` — fifteen `if (verb === …)` blocks in one 138-line method — is `verbs_get()`, a table from each word to its own method (`verb_enter`, `verb_replay`, `verb_layout` …), and `control` looks the word up, waits while a flight is under way, and answers the usage line for a word it does not know; the constructor's eleven frame pills are `frame_wire`, its Esc `escape_wire`. The DAG pane's `envelope_observe` (92 lines) dispatches an indexing answer to `feedIndexing_take` and a graph to `feedDag_take`. And the ceilings arrive for argus, as they did for orrery in slice 3: law a-part-stays-a-part, lint check of the same name — no function past 150 lines, no file past 1,200, arrow functions bound to a name measured like declarations. A module factory (`*_wire`, `*_build`) is a boundary measured by the named functions inside it, all of which already stand under the ceiling. Nine things still stand past one — the host file and its closure, the language dispatcher, the slab scene's setup, five panel and facade files — each named in the lint with its reason, its next cut, and the length it stood at; it may shrink and never grow, and anything new past a ceiling fails CI (proved with a 162-line probe). No behaviour moved: the universe, census, replay, roster, node-dive and diagram smoke stages answer as before. The epic's end checks: a clean install from a fresh clone (npm ci, build, every package's tests, both lints) is green; the full smoke on a fresh daemon answers 287 ok, 20 failed. The pre-epic build's full run could not be compared whole: it dies at node-dive on the redeclared const the epic fixed (#823), after 112 ok and 11 failed. Every red of the epic's run was therefore checked on its own: each is either red on the pre-epic build too, run alone (#825, #826 with three symptoms, #830, and the stale-page notice #837, all filed), or passes in isolation (an order dependence between stages); three of the pre-epic build's reds are gone (the HELP pane's two, fixed by #816, and the contrast check). No regression from the epic was found. The universe-pill-bar 'flake' was the boot's welcome landing after the console read idle; the check now waits for a still console and passes after universe-pane and on a fresh daemon alike. | `features/universe/panel.ts`, `features/dag/panel.ts`, `scripts/aegis-lint.mjs`
| 2026-10-02 (the older-page notice speaks plainly) | #837, found by the cleanup epic's full smoke: the notice worked, but its smoke check looked for words the notice no longer said. The operator, asked which wording should stand: neither — 'for an LLM both those options are terrible English'; say what happened and the cure: 'ARGUS has been updated in the background — refresh this page', with refresh a link. So the notice is that sentence, in sentence case, its word `refresh` the control that reloads (the sentence's own colour, underlined, so it clears contrast in every scheme; the separate RELOAD capsule is gone); the console says the same sentence; a bar readout says it in capitals (`ARGUS UPDATED IN THE BACKGROUND · REFRESH THIS PAGE`). The words live in `STALE_PAGE_WORDS` and the smoke reads them from the module's source, so the check and the notice cannot drift again. | `app/stalePage.ts`, `lcars/argus.css`, `tests/smoke/smoke.mjs` (stale-page)
| 2026-10-02 (a download is proven where ARGUS hands it over) | #830, found by the cleanup epic's smoke: DOWNLOAD's two checks found nothing on disk. Probed, not guessed: headless Chromium 152 saves no download at all — `Browser.setDownloadBehavior` on the page socket and on the browser socket, `Page.setDownloadBehavior`, an evaluate with a user gesture, a plain blob anchor; a disk-wide search found nothing — so the check measured the browser, and silently, because the smoke driver's `cdp` resolved a refused call's absent result as if it had worked. The operator's decisions: prove DOWNLOAD at the surface's hand-off, and make the driver's refusals loud. The stage now wraps the anchor's press and records the name and the bytes ARGUS gives the browser (a blob URL holding `a,b\n1,2\n` under `smoke-download.csv`), and that nothing is handed over for the file the session can no longer read while the console says why; the browser's own save is the browser's. `page.cdp` rejects with `cdp <method>: <the browser's words>`. Every stage that makes raw CDP calls (the image stages) still answers green. | `tests/smoke/smoke.mjs` (file-download), `tests/smoke/driver.mjs`
| 2026-10-02 (the form stands on its grid again) | #825, found by the cleanup epic's smoke and older than it. Measured in the browser, not guessed: the form and the levels read the same template (`5.2rem 5.2em 13em 7.5em …`), whose leading `5.2rem` is the control column the façade mints for a row's kind capsule; the form, written before that column existed, had no cell for it, so every term stood one column left of its cap (PATIENT typed over the fold, MRN over PATIENT). A first guess — different fonts making the em tracks differ — was measured wrong (the cap row is 17.6 px like the form) and reverted. The fix is a cell for the control column; and, by the operator's decisions, the shared leading tracks (fold, PATIENT, MRN) are declared once in `features/pacs/tracks.ts` (`PACS_LEAD_TRACKS`) and read by the patient and study levels, while `a-fact-has-one-source` now fails a level trait that spells its own fold/PATIENT/MRN width (proved with a re-spelling) and a form whose leading cells are not control, fold, PATIENT, MRN. The panel stays within its ceiling allowance. | `index.html` (`#pacs-form`), `features/pacs/tracks.ts`, `features/pacs/panel.ts`, `scripts/aegis-lint.mjs`
| 2026-10-02 (a question is answered where the hand is) | #826, found by the cleanup epic's smoke in three scenarios (process-form, process-workflow, select-mode) and older than it. Since questions moved onto the pane that asked, the console only noted one as text ('? Feed title:'); an operator who answered at the prompt — as the smoke did, and as anyone might — ran their answer as a command ('smoke process run' → an error) while the pane waited forever. The operator's decision: the console answers it too. `question_stand` (`app/asks.ts`) stands the question on the pane and on the console's line (`ask_open` with `focus: false`, so the pane's field keeps the keyboard) and races them; the console winning abandons the pane's bar, the pane winning settles the console's question with the pane's answer (`ask_settle`), so the transcript records it and the line is free. When the console is already asking something of its own, the pane alone asks and the console notes it, as before. Every pane question goes through it: RUN's title, a pressed verb's own question (rm -i), MKDIR's name, and the cohort band's name and CLEAR confirm. The three scenarios pass as written; a new one, question-both-places, answers a text question on the pane, another at the console, and a confirm on the pane (a file — a directory's rm -ri asks twice, as POSIX does). Law a-question-is-answered-where-the-hand-is. | `app/asks.ts`, `app/cohort.ts`, `console/terminal.ts`, `tests/smoke/smoke.mjs`
| 2026-10-02 (an editor's save is a line) | #831, slice 3, grilled with the operator: "the argus editor should appear in a pane, and in AEGIS verbs should appear in the right frame", and the kernel's `edit` is the editor everywhere ("If you edit /proc/jobs/feed_N/note from terminal, it opens a local EDITOR... For argus, it opens in the argus web editor. Same experience"). The kernel's `edit` request names its file and a surface may answer that it opened (slice 2); argus now answers with an EDIT pane split beside the focused one (`app/editor.ts`). The field is CodeMirror 6 as a guest, loaded on demand, its colours the theme's tokens; SAVE and REVERT ride the mode frame, lit only while the field differs from the file; the bar says DIRTY or SAVED; Ctrl-S saves and Esc gives the keyboard back. Every save is the line the operator could have typed, `touch --withContents='<text>' '<path>'`, echoed and run, so the transcript holds each one and a feed's note is written through its projection. A second `edit` of a file finds its pane and leaves unsaved changes standing; a desktop card brings the pane back on its file, read again. The HELP pane's build and open moved to `app/helpPane.ts` on the way, so the host shrank as the editor arrived. Law an-editors-save-is-a-line; components.adoc gains the Edit pane. | `app/editor.ts`, `app/helpPane.ts`, `features/edit/`, `app/desktop.ts`, `app/keys.ts`, `index.html`
| 2026-10-03 (a tag is a mark, and a line) | #831, slice 4b, from the tags grill (2026-10-02): "I can imagine selecting a dag and in the frame having ADD NOTE and ADD TAG... there should be a pill list of tags to choose from and also the ability to add new tags." The kernel's roster model carries each feed's tags from the cached tags index (one read per tag); the runs roster draws them as marks after the title (`features/dag/roster.ts`, the roster's traits cut out of the DAG panel), a press filters by the tag through the façade's new filter-only `terms` (`tag:`), and the × on the indicated row takes it off. The row offers NOTE (the editor on `/proc/jobs/feed_N/note`) and TAG; TAG asks a `choose` question (`features/ask/paneAsk.ts`): the user's tags (`ls /proc/tags`) as pills, the worn ones dimmed, a field for a new name, DONE to end; each pick runs `setfattr`, a new name `mkdir /proc/tags/<name>` first, and the question comes back with the tag held. A graph on stage reads its feed's note and tags when entered and shows NOTE (ADD NOTE / EDIT NOTE), TAG and the marks under BACK; an editor save of the note reads them again. The operator, during the slice: notes are resolved only when a feed is entered (CUBE gap filed, ChRIS_ultron_backEnd#737). A first build put the marks in the row's honey on the indicated highlight, unreadable, and the frame's verbs below its fold; both were moved before merge. The feed row's handlers moved to `app/feedRows.ts` and a visible line to `app/lines.ts`. Law a-tag-is-a-mark-and-a-line. | `app/tags.ts`, `app/feedRows.ts`, `app/lines.ts`, `features/dag/roster.ts`, `features/dag/panel.ts`, `features/ask/paneAsk.ts`, `features/roster/order.ts`
| 2026-10-03 (a text file offers EDIT) | #831, slice 3b, as grilled: "for editable files (like txt/json/yaml) etc a file select should expose an EDIT in the frame, too", with "are you sure" at a megabyte or more. A file row whose extension is not on the one binary list offers EDIT, a feed's note seen through /proc included; the list moved into the contract package (`EDIT_BINARY_EXTENSIONS` in @fnndsc/menu), read by the kernel's `edit` for its refusal and by the surface for its offer, so the two cannot disagree. EDIT runs `edit '<path>'` as a visible line and the kernel opens the pane; a file of `EDIT_CONFIRM_BYTES` or more asks on the pane first (`editAsk_render`). New smoke stage edit-row. | `app/browser.ts`, `features/roster/verbs.ts`, `features/edit/line.ts`, `packages/menu/src/edit.ts`
| 2026-10-03 (a field answers its edges) | The operator: "On scroll over a list by mouse scroll, finger, or N MORE when it hits an edge top or bottom can we have a slight flare pulse effect and a bounce like on mobile devices" (and: LCARS makes no mention of such a thing, so it is ours to define). `more_wire` (`features/roster/more.ts`), which every scrolling field already wears, now notices an edge met — a scroll come to rest at the top or the foot (the wheel, a finger, an N MORE press that reaches the end), or a wheel or a finger pushing past an edge the field already stands on — and says which (`data-edge`), resting 450 ms so a spinning wheel is one meeting. The stylesheet flares that edge with an inset glow in `--edge-flare` (the theme's harvest gold) and gives the field's contents six pixels of spring; the field's own frame never moves, and `prefers-reduced-motion` draws neither. Written as state the stylesheet reacts to, so single-animation-author holds. The first build's glow was a hairline nobody would see; it was broadened before commit. New smoke stage edge-flare. | `features/roster/more.ts`, `lcars/argus.css`, `tests/smoke/smoke.mjs`
| 2026-10-03 (a holder is a mark, and a line) | #853, slice 2, grilled with the operator: "I feel like these mise gaps should be fixed now while we are at it." With the kernel speaking every entry CUBE has (#854), the runs roster's SHARED WITH readout becomes marks — a user, a group, PUBLIC — each with a × that asks before `setfacl -x` (or `o::-`) runs; SHARE becomes a choose question (the operator's groups from /etc/group and EVERYONE as pills, a field for a user, re-asked until DONE); RENAME arrives on the row and the graph's frame and writes the title file, the bar reading the new name at once. The listing façade's readout takes an element as well as words. Found while building it: a press in a pane's question bar retracted the mode frame and so dropped the row's indication (fixed: a question is not the field), and the console piled consecutive visible lines' answers into the first one's block (fixed: an echo closes the stream). The graph frame's feed verbs moved into `features/dag/feedFrame.ts` to keep the DAG panel under its ceiling. Law a-holder-is-a-mark-and-a-line. | `app/access.ts`, `app/feedRows.ts`, `features/dag/feedFrame.ts`, `features/ask/paneAsk.ts`, `features/roster/listing.ts`, `console/terminal.ts`, `app/main.ts`
| 2026-10-03 (ARGUS and the calypso daemon say when they are different builds) | The operator, on pangea: "I clicked on a file in ~/me.txt -- the frame opened and showed EDIT -- I clicked that and NOTHING happened." Nothing in EDIT was broken: the operator's calypso daemon had run since 2026-10-01 while ARGUS was rebuilt beside it. The daemon serves ARGUS from disk on every load but runs the code it started with, so a freshly loaded ARGUS asked an old daemon for something it did not know, and nothing said why. Under honest-wait, ARGUS's build now writes its stamp (`build.json`: commit and minute) into the bundle; the calypso daemon reads it once when it starts and reports it in its attach ack (`stack.surface`); ARGUS compares its own stamp and, when they differ, says which is older and what to restart — on the status strip (`CALYPSO DAEMON OUT OF DATE` / `ARGUS OUT OF DATE`), in a dismissible notice over the stage, and on the console beneath the greeting with the two builds as evidence: `ARGUS is newer than the calypso daemon, so some controls won't work. Restart the calypso daemon.` or `ARGUS is older than the calypso daemon. Refresh to load the new ARGUS.` (Refresh is the control). The words name the two processes, never "session" or "page" — the operator's correction of the first draft. A daemon too old to report a stamp counts as older. A remote chell says the same at attach when its build and the daemon's differ. The boot's greeting moved to `app/greeting.ts` so its notes can follow it, and the host's ceilings ratcheted down (2,913 lines; `surface_start` 1,941). | `app/buildMatch.ts`, `app/greeting.ts`, `app/status.ts`, `app/main.ts`, `vite.config.ts`, calypso `daemon/static.ts`, chell `remote/buildMatch.ts`
| 2026-10-03 (a run left by Esc comes back unlit) | The operator: "Select a run in the DAG listing. That listing highlights and the others go dark. OPEN that DAG. Then press ESC to return to the DAG list. It still shows the selected dag with others dimmed. There seems NO WAY to remove the selection." It was not the indication: OPEN stands that down. It was the press's acknowledgement, `listing-activating` (a-press-is-acknowledged): the pressed row lit, the rest dimmed, OPENING on the bar, until the answer lands. The façade only stood it down when the listing's own rows arrived (`rows_set`), and a run's answer is a graph taking the pane, so it stayed. The roster came back still wearing it, and since it was not an indication, neither a press off the listing nor Esc could clear it. The façade's `activation_settle` is public now. The DAG pane calls it when the graph lands, when the feed is refused, and when Esc backs out before the graph arrives. A press off the listing already stood an indication down, as the zone's contract says. New smoke stage roster-returns-unlit, which fails on the old pane on both checks. | `features/roster/listing.ts`, `features/dag/panel.ts`, `tests/smoke/smoke.mjs`
| 2026-10-03 (the calypso daemon knows when it is stale) | #863, slice 1, grilled with the operator after #858's warning cried wolf: an ARGUS-only release (0.21.1) changed no daemon code, yet every running daemon read CALYPSO DAEMON OUT OF DATE, because the rule compared ARGUS's stamp alone. The one that can answer "is this process still running the code on its disk?" is the daemon, so it answers. It fingerprints its own packages when it starts (each one's version and the content of its compiled `dist/*.js`; content, not file times, since a reinstall of the same version rewrites every time without changing a byte, while a checkout rebuilt at the same commit changes bytes without changing a version). It reads the fingerprint again on every attach and once a minute, says `stale` in the attach ack, and pushes `stale` to every surface when the answer flips. ARGUS believes it: stale reads CALYPSO DAEMON OUT OF DATE, and the sentence says what happened ("The calypso daemon has been updated and should be restarted; some controls may not work until then. Restart the calypso daemon." — the operator's wording, replacing "ARGUS is newer than…", which no longer described it); not stale but a newer ARGUS is silent; an older ARGUS still reads Refresh. A daemon too old to say falls back to the stamp. An open ARGUS learns of an upgrade within a minute, without reattaching. The pane-is-the-subscription law's text now says the kernel never polls CUBE: a minute's read of local disk is not CUBE traffic. | `app/buildMatch.ts`, `app/status.ts`, `app/main.ts`, `calypso/client.ts`, calypso `daemon/codeIdentity.ts`, `daemon/server.ts`, `daemon/launch.ts`, menu `messages.ts`
| 2026-10-03 (the calypso daemon is restarted by its own operator) | #863, slice 3, grilled with the operator, who said: "It's probably better to not restart a session from under a user, right? Having them see the warning is a good thing … Does the warning currently allow the user themselves to restart?" It did not, so now it does, behind a door (`app/restart.ts`). A RESTART pill stands beside LOG OUT, and when the daemon is stale the notice's cure "Restart the calypso daemon" is a control, as Refresh is in the older-ARGUS notice. Both raise one question in the notice's spot: "Restart the calypso daemon? You'll stay logged in; the page reloads when the new daemon is up." Under it, "This will cut off:" lists only what applies now: the command holding the lane (from the heartbeat), each unsaved edit (by file), and the other surfaces (the heartbeat now lists them by id and kind, and the attach ack gives each surface its own id). YES is focused; Enter answers yes and Esc no, and neither key reaches the stage. YES posts to the door's `restart`, which answers with the greeter, and the browser follows the boot back in. Another ARGUS on the same session, told `closing` with cause restart, says "The calypso daemon is restarting (asked from another browser or surface). Following it…" and follows to the greeter itself. A page with no door shows neither control, and the notice's cure stays words. The LOG OUT pill's wiring moved to `app/door.ts` and the cycler's /bin seed to `app/cycler.ts`, which keeps the host under its ceilings (ratcheted). Proved live on a private porter, with two browsers: the question named "1 other browser"; NO left everything as it was; YES went through the greeter to a fresh daemon (new pid); the second browser said why and came back on its own; and the stale notice's cure opened the question. Law a-daemon-is-restarted-by-its-own-operator; smoke restart-question. | `app/restart.ts`, `app/door.ts`, `app/buildMatch.ts`, `app/cycler.ts`, `app/main.ts`, `calypso/client.ts`, `calypso/routes.ts`, `index.html`, `lcars/argus.css`, calypso `daemon/server.ts`, menu `messages.ts`
| 2026-10-03 (the smoke suite runs green, and says who leaked) | The operator, shown 12 reds in the epic's full run: "I don't like the 12 failed tests … the goal is to fix the underlying issue so tests can run green." The full suite now answers 317 ok, 0 failed. Four reds were tests describing an older surface: console-height dragged with mouse events on the window, after the strip moved to captured pointer events for phones; mode-frame counted the field's MORE chip as a stray control and expected a sliver strip where the spine is now drawn at `--spine-w`; and enter-place and live-watch entered a run with a single click, which has indicated since #454 (they passed only because an earlier scenario had left a graph on stage). Eight were scenarios breaking the next one, and under them were five real defects. (1) `feed rm` deleted the feed in CUBE but never told the session's index, so the RUNS roster kept listing it and a verb on that row met "feed not found"; cumin's `feed_delete` now drops it from the proc cache. (2) The RUNS pane recorded the cwd only when it followed, so the cwd went stale while the roster stood, and the next promptline after a pick read a move from minutes ago and replaced the pick; the cwd is now recorded on every promptline. (3) A follow asked before a hand pick but answered after it took the pane as if it were the pick ("chose 134, shown 21"); `features/dag/follow.ts` drops the superseded follow's answer. (4) RUNS pressed over a graph left the canvas under the roster, the old feed in the title and its watch still sampling; every way back to the roster now leaves the graph whole (`graph_leave`). (5) A feed listing brought by some other command pulled the roster up over a graph; it takes the stage only when asked for, or onto a roster. The harness now names leaks: after every scenario it checks the session is back at the suite's starting cwd, with nothing lit, no question open and no RUNS pane left on a graph. It fails the scenario that leaked, once, and puts the session back. Scenarios leave as they found it through one in-page helper (`home_return`). image-guard shows its series afresh, since LOAD is consent for one showing of a series. | `features/dag/panel.ts`, `features/dag/follow.ts`, cumin `feeds/chrisFeed.ts`, `tests/smoke/smoke.mjs`
| 2026-10-03 (a missing fact reads as nothing, never as a crash) | Found when the smoke suite's PACS and GATHER stages first ran with real fixtures. (1) GATHER on a PACS series did nothing at all, with no notice and no count: the cohort held a gathered PLACE (\`gather add <dir>\` in the console), whose description is undefined, and the listing façade handed that undefined cell to appendChild. The throw took the whole render, and so the GATHER, down silently, every time after. The façade now draws any cell that is not an element as text, with nothing for a missing fact, and so does its cell-text helper (which sorting and filtering read). A gathered place reads as its path. (2) The PACS query form stands on the study grid (a-form-stands-where-its-answer-will), but it sits outside the results field, so it lacked the field's spine and gutter and the rows' elbow clearance. Its flexible PROGRESS track ran 68 px wider, and DATE, ACCESSION, MODALITY and SERVER stood right of their caps. The form now ends where the study rows do, from the same tokens. | \`features/roster/row.ts\`, \`features/gather/panel.ts\`, \`features/pacs/panel.ts\`, \`lcars/argus.css\`
| 2026-10-04 (2D flattens what is in view) | The operator, inside a feed in the UNIVERSE, pressed 3D and the whole galaxy flattened: "I would expect that to change the compute \"molecule\" on display to be 2D since that is what is focussed … this toggles the whole galaxy to be 2D? this is not what I or a user wants." Inside a feed the scene still holds the whole space, the feed swapped for its graph and the rest dimmed, and the projection was one setting for all of it. Now the 3D / 2D block acts on what is in view: inside a feed, that feed alone settles flat on the plane through its centre that faces the eye, keeping the spread it had, the camera reframes it, the idle spin rests, and the space behind keeps its depth; outside, the whole space flattens as before. Agreed with the operator: each feed is entered in 3D, and BACK leaves the flat reading with the feed. Orrery carries it as `flat_set(ids)`; a new graph ends it.
| 2026-10-04 (a function fits a screen; a file only warns) | The operator, after the line ceilings forced lines squeezed to meet a count while landing feed-only 2D: "have you just defined arbitrary file line limits here for files?" They had come with the cleanup epic (#814): 150 lines a function, 1,200 a file, and every file already past 1,200 frozen at its length that day. Agreed with the operator: the function ceiling stays a law ("my rule of thumb is to have functions fit in one screen fully … one 4k landscape screen with a neovim terminal"); a file past 1,200 is named as a warning in `lint:aegis` and `lint:orrery`, never a failure; the frozen per-file caps are gone. The three long functions keep their caps. |
| 2026-10-04 (a first screen is the dashboard alone) | The operator, on ARGUS for new users: "my fear is that the chrome, colors, terminal, left page frame etc is just too overwhelming," then "the default first view should be only the dashboard zoomed in. No header frame, no side body buttons, just the dashboard," with "a few lines of more descriptive text for each tile." Built as said: a session that begins at the dashboard begins with it zoomed, so the header, the gutter and the console are away; any press opens what it names and gives the full surface back, and Esc, the top strip or anything else taking the stage does too. Each block now says what it is for in the operator-approved words (`features/launcher/about.ts`; ANALYSES names the ChRIS word "feeds" once; UNIVERSE lists its layouts). The operator on the old HELP block: "it's not really HELP -- it's keyboard shortcuts. Also it confusingly opens a file roster AND the help text" — the dashboard had yielded to home and HELP split beside it; the block and the pane are KEYS now, and from the dashboard the keys stand alone. A CONSOLE block is the typed door. On a phone a block is as tall as what it says. |
| 2026-10-04 (a double click on the frame zooms the pane) | The operator: "I kinda find myself wanting to double click the 'header' frame of pane to toggle between zoomed in and zoomed out modes." Every desktop's title bar does it, and the zoom it toggles already existed (the drawer's ZOOM / RESTORE capsule, the shared `data-pane` path). A double click on a pane's frame now presses that capsule; its two single clicks have opened and closed the drawer, which ends as it was. No machinery joins the bar (`bar-no-machinery` holds: the gesture belongs to the handle itself), the title's words are not left selected (`user-select: none`), and a double tap on a phone zooms the pane, not the page (`touch-action: manipulation`; probed with emulated touches). ZOOM stays the visible way; the `z` chord's line in KEYS names the double click. |
| 2026-10-04 (the console is a pane in this too) | The operator, the same day: "while 'panes' seem to now honour the double click zoom/unzoom... the console does not. It should too. Also when full zoomed, the bottom right console frame rectangle that opens/closes the console in full argus view does nothing when console is 'full screen'." Both fixed. The console's frame zooms on a double click and restores on another, its drawer ending as it was. The lid, visible in the console's zoom, did nothing there because the zoomed console's height is the zoom's; it now gives the stage back first and its own press then closes the console — and CLOSE in the console's drawer, which rides the lid, closes a zoomed console too. Smoke: the console's double click and the lid in zoom. |
| 2026-10-04 (an ERROR that the files contradict is not an error) | The operator, on a PACS series reading ✗ ERROR: "this seems to happen more often than not of late. I pull a series to download, and then I get some error state." Investigated: the series was 131/131 in CUBE. Two defects. The kernel's watch called a series unconfirmed when nothing arrived for fifteen seconds, and with no retries (what ARGUS runs) the step meant to ask CUBE sat inside a loop that never ran, so the series went straight to ERROR while the PACS kept sending. Now CUBE is always asked; a series not seen to finish and not yet home is UNCONFIRMED with its reason, not ERROR; the quiet window is a minute. And the pane let the wire's last word outrank a fresh answer finding the series home: an answer that finds it in CUBE now replaces a pull that ended badly (never one in flight). A badge that ended badly carries the kernel's reason on hover, and the console's summary prints it. The operator also caught the waiting animation gone: a fired pull says its total at once, so a series waiting on its first file drew a still 0% bar; it paces again until the first file lands. |
| 2026-10-04 (a silent PACS is the operator's call) | The operator, watching a test pull give up on its own: "after say 20 seconds ARGUS should, after verifying files have not landed in CUBE, pop up a 'No response yet... continue to wait Y/N'." Built so: after twenty seconds of silence from the PACS the watch asks CUBE first (a series that landed unseen is done), then asks the operator "No files from the PACS yet … and none in CUBE. Keep waiting?"; YES waits another twenty and asks again, NO ends the series UNCONFIRMED with the reason. Walked live: asked at 20 s, YES, asked again at 44 s, NO, the badge settled with its reason. Building it found the pane's PULL and query running as instrument traffic, which the session may not let ask a question — wrong, since the session's own rule is that a command changing state is never instrument traffic: the operator's acts now run as theirs (`line_execute`'s `operator`), still quiet in the console. Found on the way: pfdcm records how many instances the PACS moved for each retrieve; a series it moved none of will never arrive, and saying so outright (rather than asking to wait) is the next step. |
| 2026-10-04 (a block's press opens only what it names) | The operator, pressing KEYS on the new first screen: "the response of the whole ARGUS folding open, including an open console is too much … the header frame stays closed so only the left body gutter is shown and the console stays closed unless opened. Most users won't want it and those that do can find or be told." So a press on a dashboard block now gives back the gutter and the pane it opened, with the header folded away (its strip brings it back) and the console closed (CONSOLE-05 or the CONSOLE block opens it). Esc on the dashboard, the operator's own way out, still gives the whole surface. |
| 2026-10-05 (one field, one chip) | Seen in a screenshot of the dashboard with the console open: two ▼ MORE chips stacked at its foot. The grid scrolls on a desktop and the body scrolls on a phone, and both were wired; the grid's chip, mounted in the body, made the body look overflowing, so it spoke too. A `more_wire` field may now say when it is the one that scrolls (`scrolls`), and the dashboard's body speaks only where it does. |
| 2026-10-05 (a zoom is a zoom, however the page got there) | The operator, from a files pane reached by the dashboard: opened the console, pressed its ZOOM, "expecting a full page zoom. Instead … the top header frame opened and zoom was not a real zoom." The header's slide distance was measured from its painted box, which carries the slide itself: the dashboard letting go of its zoom left the header still gliding back, it measured a few pixels, and every later zoom slid it up by those few. It is measured by layout now (offsets ignore the translate). Smoke reproduces the path (399 px of header on screen before, none after). Found beside it: KEYS opened from the dashboard stands in home's place, so FILES-02 believed it was already home and sent the gutter away instead of bringing the files back; a gutter press is "already there" only when its domain's own pane is on stage. |
| 2026-10-05 (the games shelf) | The operator, shown a list of small BSD-style utilities to add beside fortune, cal and weather: "Call me crazy but I kinda want all. I'd add their help to a separate listing page though, like 'help bsd' or something would list them grouped into categories like the current help." Settled: the page is `help games` — the BSD word; `/usr/games` follows from it — and cowsay's cow is the ChRIS brain. Epic #892, five slices on one branch. Slice 1: the shelf (`builtins/games/shelf.ts`) names the commands by category and nothing else; `help games` lists those that exist, `ls /usr/games` walks them, `cat /usr/games/<cmd>` reads the one help entry each command has; the KEYS pane carries a GAMES block naming the way there. A name on the shelf with no help entry is not listed, so the page never promises a command that has not landed. |
| 2026-10-05 (a pipe of builtins is the kernel's) | Games shelf slice 2 (#892): cowsay with the ChRIS brain, cowthink, figlet and banner (one five-row face), lolcat, rev, tac, yes (bounded, and it says so), seq, factor, primes, shuf, roll, rot13, morse, calc (its own arithmetic reader, no eval) and units. Building `fortune \| cowsay` found that a pipeline's later segments always went to the surface's host shell — so the toy would have reached the host's cowsay, and a browser refused the line outright. A segment that names a builtin now runs in the kernel and reads what came before it (a piped-input slot set for one segment); only a segment naming something else goes to the host, and a browser refuses that one in words. Smoke: the page, the brain, a chain, and the refusal. |
| 2026-10-05 (the lab's own) | Games shelf slice 3 (#892): `wtf` (BSD's acronym expander with the lab's book: ChRIS, CUBE, pfdcm, oxidicom, LONK, PACS, DICOM, MRN, SeaGaP, the mise packages), `chrisfetch` (the brain beside the session's facts, from what it already holds), `who` and `uptime`, `ping` (round trips to CUBE only — a hosted session is not a probe for its network), `file`, `xxd`, `strings`, `sha256sum` and `md5sum` on CFS files (a projected file is read as text), `qr` (an encoder of its own, byte mode, levels 1–10 at L, checked module for module against a reference and read back by zbar for every version), and `say` (the kernel hands the words on; ARGUS speaks them through the page's speech, the console prints them). `df` left the shelf: CUBE has no storage or quota endpoint, and a `df` that walked every folder would not be `df`. The kernel gained one seam for `who`: a surface may name its peers (`Surface.peers`), which the daemon answers from its attached surfaces. `file` became a shared subject in the ARGUS language, as `help` and `tags` are. |
| 2026-10-05 (time and sky; a chime is a word said later) | Games shelf slice 4 (#892): `pom` (BSD's phase of the moon, drawn), `stardate` (the Kelvin reckoning, TNG's beside it), `ddate` (the Discordian calendar, holy days and St. Tib's Day), `sunrise` (Open-Meteo, the service `weather` uses), and the chimes: `timer 5m tea`, `leave +0030` (BSD's three words: five minutes before, at the time, a minute after), `stopwatch`. A chime holds no lane: the kernel keeps it on a clock and, when it sounds, publishes it on the ambient channel as an envelope of its own — the road a refreshed listing takes — so every surface hears it; ARGUS notes it in the console and speaks it. Smoke: a two-second timer chimes into the console. |
| 2026-10-05 (the stage has a canvas for play) | Games shelf slice 5 (#892): the showpieces `sl` (a starship, as the train was a typo's punishment), `cmatrix`, `rain`, `asciiquarium`, and the arcade `tetris` and `snake`; `quiz`, `hangman` and `2048` play by question and answer through `repl_question`, so they run anywhere a question can be answered — a terminal, the ARGUS console's ask bar — and the rules are pure functions under test. The six that move answer a `games.show` model: a console with no canvas prints a still and says where the moving one is; ARGUS opens a GAMES pane — a canvas of monospace glyphs in the theme's hues, a clock stopped while hidden or paused, a field that takes the keyboard when a game starts and gives it back on Esc (focus-stays-in-the-field, the image pane's grammar; its step in the keys ladder). A second program reuses the pane on stage. The pane is a component with its reference (components.adoc, Games pane) and a program is a plain object with step, draw and, for a game, key/status/over, so the arcade grows by a factory, never by a word the surface claims. | `builtins/games/show.ts`, `builtins/games/play.ts`, `features/games/{program,shows,arcade,panel}.ts`, `app/gamesPane.ts`, `tpl-pane-games`, `app/keys.ts`, smoke `games`
| 2026-10-05 (the ship is the Enterprise; the shelf is in the main help) | The operator, after the games shelf published: `sl` should fly an ASCII Enterprise, the surface being LCARS already; and bare `help` named the shelf only in a grey footer line while fortune, cal and weather sat under General — "shouldn't weather and fortune be in games?" Now bare `help` has a Games and utilities group (fortune, cal, weather, motd) with a `help games` row whose count of the rest is read from the shelf, General keeps help, date, exit and `!`, and `sl` flies the Enterprise: a first drawing of my own read as a bus, so the operator asked for found art — the Constitution-class refit side view from the Star Trek ASCII Art blog (re-use granted with attribution, which the source keeps), the same rows in the kernel's still and on the GAMES pane, left to right, the wake behind the nacelle. | `builtins/help.ts`, `builtins/games/show.ts`, `features/games/shows.ts`
| 2026-10-05 (what's new, for the operator) | The operator, after two publishes in a day: "Should we move to release notes? Would there be utility in having these in a release notes pane (or in the dashboard)?" Grilled and settled (epic #897): the notes are the changesets, written once — a changeset's first sentence is the operator's headline (lint-held: ≤120 characters, no leading code name, no dev vocabulary; "Internal: …" marks ours alone) — shipped as `dist/notes.json` by every package an operator touches, a release's date and identity the Version Packages merge that published it. The kernel's `notes` reads the disk the daemon runs from and answers a `session.notes` model; `help notes` and `/usr/share/doc/NEWS` say the same. On the stage: a WHAT'S NEW dashboard block (the date, the count, the first three headlines; AFTER RESTART when the daemon's disk moved) opening a NOTES pane on the listing façade — one block per release, a row per headline, the rest unfolding on a press, LATEST/LAST 5/ALL and INTERNAL pills that ask the kernel again — and one line on the stale-page notice naming the page's own newest release and headline, since the page is the one ahead. Law: a-readout-names-the-release-it-reads. | `scripts/notes-build.mjs`, `scripts/changeset-headlines.mjs`, `builtins/sys/notes.ts`, `menu/notes.ts`, `features/notes/panel.ts`, `app/notesPane.ts`, `app/dashboardTiles.ts`, `app/buildMatch.ts`, `tpl-pane-notes`, smoke `notes`
| 2026-10-05 (the full run's long-standing reds, read at last) | Four checks had been red in every full smoke run since the 2026-10-03 baseline and were waved through as "pre-existing" (#899). Read with their detail printed: the mode-frame and CARDS checks ran on a browser the games scenario had left at `/usr/games` (a typed `ls` projects its listing into the browser), where the path's crumbs are buttons in the field and the `~` lead row is not a card — the law had only ever been checked at home; the crumbs are the listing's own frame as the caps are, and the lead rows are not rows, so the checks now say so. The stale-page check counted any console line quoting its sentence, which the release notes now do. And `restart-question`'s leftover was the gather band's open cohort, indicated by design while folded: the hygiene check counts lit rows on stage. One real defect among them: `file root` typed with an image pane focused reached the image pane; a kind-named subject now acts on the first pane of its kind on stage when the focused one is not (kindPane_resolve), as `file sort` already did. | smoke `mode-frame`, `cards`, `stale-page`, `stage_leaks`; `console/argusLang.ts` (`kindPane_resolve`)
| 2026-10-05 (the expanse gives last) | The operator, on a tablet: the runs roster's PROGRESS column squeezed to a sliver and CREATED clipped off the right — "is this a regression?" Not one: TITLE had been a fixed 24em since the expanse rule (2026-09-08), and on a pane narrower than the fixed tracks' sum the one `1fr` collapses first. Settled: a narrow pane trims the title (ellipsis) before the expanse goes — TITLE `minmax(12em, 24em)`, PROGRESS `minmax(8em, 1fr)`; a wide pane is unchanged, so bars keep their alignment across levels. Still open: what a pane narrower than the fixed floor does with the trailing tracks (clip today), and whether columns become hand-sizeable. | `features/dag/roster.ts`
| 2026-10-05 (columns sized by hand) | The operator, after the narrow roster: "in most file managers the cols are user sizeable. what would it take to do here?" One façade change, every listing: a grip on each cap's right edge (`columns.ts`) sizes that column by drag within its declared floor (`minmax`'s min, else 3em), a double-press gives one back, and a COLUMNS block on the frame reads `SIZED` and gives all back; the expanse is never grabbed and takes the difference, so the bars' alignment survives (law the-expanse-is-never-sized-by-hand). Grilled: memory is the device's (`localStorage` by prefix and trait — a tablet and a desk want different widths; kernel-owned defaults when that slice lands), the way back is both the grip and the block, fixed tracks only. The grip is the gutter between caps (the operator: "a user expects to mouse/touch in the slight gutter between col headers"), wide enough for a finger, so a touch drags it too. | `features/roster/columns.ts`, `listing.ts` (template takes the sized px), `order.ts` (grips minted with the caps), the `<prefix>-columns` block in every listing template, smoke `listing-columns`
| 2026-10-05 (a login never lands on an older kernel) | The operator, on titan after four publishes: `chell` through the door landed on a day-old daemon with no `/usr/games` — "a recurring issue that you claim is solved but clearly isn't". Right: every publish had left every running daemon on old code, and the only cure was a warning and a hand restart. Now the berth carries the daemon's versions (calypso) and porter compares them with its own tree at every login, ending a behind daemon and booting afresh on the way in; a berth without versions predates the field and counts as behind. The warnings stay for the one case left, code moving under a session in use. | `calypso/daemon/launch.ts` (berth versions), `porter/versions.ts`, `porter/app.ts` (login)
| 2026-10-05 (against rot) | The operator, after seven publishes and a dozen fixes in a day: "the code base is showing signs of rot. What is going on?" Read apart, the day's bugs were four kinds — a design gap exposed by cadence (stale daemons), tolerated test debt (four reds carried as pre-existing), a verification rig too narrow (one daemon, one identity, 2560 px, a mouse) and first-cut bugs that shipped before a second look — and two structural signs (two files living on size warnings; facts spelled in two places; the deploy procedure outside the repo). Epic #915. Landed first: the nightly zero-red smoke with its ledger, the wider rig as a law and as `SMOKE_WIDTH`/`SMOKE_TOUCH`, and `make deploy` carrying the verify-before-remove rule into the repo. Next: the upgrade-path smoke, one publish a day, the ratchets paid. | `scripts/smoke-nightly.sh`, `scripts/deploy.sh`, `tests/smoke/driver.mjs`, Makefile `deploy`/`smoke-report`
| 2026-10-01 (a drawer verb has a chord) | The operator: "lots of new actions have landed without corresponding keyboard shortcuts — do a pass, inspired by tmux." Before: Ctrl-B opened the drawer and every verb was prefix, N tabs, Enter; `pane focus left|right|up|down` had no key at all. Now, with the drawer open, one key presses one capsule — tmux's where tmux has a word (`%` `"` split, `z` zoom, `x` close, `o` next pane, `:` the line), vim's letters to split (`h j k l`) and capitals to MOVE (`H J K L`, arming MOVE first and standing it down when the side is dimmed), `m` arms MOVE, `u f v` the binding, `1 2 3` the claim, `?` the table. Settled with the operator: the arrows focus the pane in that direction (tmux) and Tab alone walks the verbs. One table (`DRAWER_CHORDS` in the console language) feeds the handler, the capsule titles (`[key x]`) and `argus keys`, so no parallel map drifts. Second pass, the rest of tmux's table the operator judged useful: `;` the last pane, `q` every pane's `@id` on its bar for a moment, Space flips the pane's split (beside becomes above), `!` breaks the pane out (alone on stage, the arrangement it leaves a PANES card first), Ctrl-arrow moves the boundary beside the pane a step (`pane resize left|right|up|down [percent]`, the first keyboard resize), `w` PANES, `[` the console's scrollback takes the keys; `pane flip` and `pane focus last` are the verbs typed. | `console/argusLang.ts` (`DRAWER_CHORDS`, `KEYS_HELP`, `argus keys`), `app/main.ts` (the chord handler, titles), smoke `prefix-chords`
| 2026-10-01 (a pane can be moved) | The operator: "once a pane has been split, there is no way for the user to re-arrange, right?" Right — close and reopen was the only rearrangement. Grilled, nine decisions: the operator's own design, the drawer's decorative SPLIT becoming a real pill beside MOVE, the same four direction capsules acting under whichever is lit, MOVE one-shot. MOVE <side> detaches the pane (its sibling fills the hole) and re-splits the nearest block on that side — the whole subtree beyond the nearest ancestor split on that axis that had the mover on the near side, or its former sibling when none and the axis differs — opening even; at the outer edge a refusal by name; a lone pane dims every direction. Any pane on stage moves, primaries included. The mover keeps focus, its bar reads MOVED <SIDE> (a-pill-answers-on-its-own-bar), nothing in the console from the capsule; `pane move left|right|above|below` is the verb typed, the shape of `pane split`. Offered at every width: the surface keys on width, not device, and the tree still orders the narrow sequence. Restoration (decision 7, amended in S3): births alone cannot say "beside that whole block" — replay only ever splits one leaf, in creation order — so a desktop card and a dormant group also record the stage's tiling at dormancy, leaves as action indices, and lay it over the replayed panes; the births stay what they are (what each pane is, split from whom). Epic #806: S1 the tree verb (`tree_moveLeaf`, nine unit tests), S2 the drawer and the verb, S3 restoration. | `app/layout.ts` (`tree_moveLeaf`, `leaf_move`, `move_possible`), `app/main.ts` (drawer modes, `pane_move`), `console/argusLang.ts`, smoke `pane-move`
| 2026-10-01 (a field says it holds more, everywhere) | The operator, inside `/bin/DICOM_anonymization_and_niftii_with_neuro_push_id159` then `pl-neurofiles-push`: a scrollbar, the node's parameters washing off the field — "if we are going to use the N MORE it must be consistent, right?" Right: the law said every scrolling field and four were wired. Inventory of the stylesheet's `overflow: auto` rules found eight more: the node readout immersed, the errand pane's answer, the dashboard's tiles (grid on a desk, body on a phone), the PANES desktop, the slaved viewer's body, the PACS workspace whole, and two mounts (the gather pane's rows, the CSV table's rows) that scrolled around the listing field inside them — those now `overflow: hidden`, the field inside speaks; a card's text glimpse is not a field and no longer scrolls. Every one of the rest wired. And a lint, so it stays uniform: `MORE_FIELDS` names every scrolling field; the aegis lint holds each `overflow: auto` rule to the list and each name to a `more_wire` call that names it (lint:a-field-says-it-holds-more, the law now in the table). | `features/roster/more.ts` (`MORE_FIELDS`), `scripts/aegis-lint.mjs`, smoke `more-everywhere`
| 2026-10-01 (an image fills its field) | The operator: "jpgs seem stretched. Not sure if I like that." They were: `.files-image` capped the height at 60vh inside the content view, a flex column that stretches each child to the full width — a picture taller than that at full width was flattened to fit. Law a-field-fills-its-pane already says it: the image fills the field and keeps its shape, letterboxed on the field's black (`object-fit: contain`, flex-filled, no cap), the way the IMAGE pane's field does; in the viewer's block body the same rule scales to the width and the body scrolls a tall one, saying so. | `lcars/argus.css` (`.files-image`)
| 2026-10-01 (a pill answers on its own bar) | The operator, after cycling the UNIVERSE layout pill: the console filled with the layouts' one-line descriptions ("every plugin a star, placed by what runs with what...", three full cycles of six), with no command typed — "weirdness in the console". Each pill press had narrated its answer into the console as a note. Law: a frame pill that changes a pane's own view (its layout, its captions, its replay) answers on that pane's bar — `.pane-state` in `state-note` for a moment, the standing state returning after — never in the console, which carries only what was typed or asked there. A control whose press acts beyond the pane (GATHER writes the cohort, PULL asks CUBE) still records itself in the console: that line is a record of an act, not a readout of a view. The same word typed (`universe layout clumps`) still answers where it was asked. | `features/universe/panel.ts` (`bar_note`, `state_stand`), `lcars/argus.css` (`.state-note`), smoke `universe-pill-bar`
| 2026-10-01 (the tab wears the mark) | The operator: a browser tab showing the blank document icon. The surface had no icon at all. It now wears the ChRIS logo on the page's own black ground, made from the vendored brand SVG by `scripts/favicon_make.mjs` into `public/favicon.svg` (browsers that take an SVG icon), `favicon.png` (the rest, and a phone's home screen), and porter's `src/favicon.ts`, so the door and the greeter wear the same mark as the surface they open. The links are relative, as every asset is, because the page is mounted under a door's prefix.
| 2026-10-01 (a field says it holds more) | The operator, on the band's `+N MORE · ON STAGE` chip and the frame's `▼ N MORE`: "wondering if, for consistency under AEGIS, we don't do that uniformly?" Yes, as a law, with one refinement. LCARS has no scrollbars, and a scrollbar was never the field saying anything; the chip is the field saying what it holds. But a chip is a poor way to MOVE — thirty presses through a manifest — so the wheel and the finger keep scrolling, and the chip is the readout with a page-press on it. Every scrolling field now goes through one helper (`features/roster/more.ts`, `more_wire`): the browser's bar hidden, a chip at the foot reading `▼ N MORE` while content lies below and `▲ TOP` at the end, N in the field's own unit — rows for a listing field, blocks for a frame, lines for a file's text; the band's chip keeps its verb (`+N MORE · ON STAGE`) on the same count. The listing façade wires its field as it mints it, the file view its text, the band and the frames theirs; the console's scrollback keeps its own idiom. Law: a-field-says-it-holds-more.
| 2026-10-01 (a refused read says why) | The operator, on a phone: an image pane reading `REFUSED 1 OF 1` and nothing else, for a file the node overlay had just listed. Three layers each knew a little: CUBE answered a status, the kernel noted it (`File ID n: CUBE refused access (403)…`), the daemon's `/vfs` route turned every failure into `404 not found`, and the loader could only say that it failed. Now the kernel's `file_read` throws a `FileReadRefusal` carrying the status and one line (403 not yours to read; 404 no such file in CUBE; 502 CUBE could not serve it, or the kernel's own note), `/vfs` answers with that status and line, and an image pane that lost a slice asks the route why and reads it out: `REFUSED 1 OF 1 · NOT YOURS TO READ: CUBE REFUSED THE BYTES (403)`, the same line on the console. Law: a-refusal-says-why.
| 2026-09-30 (the way out of a graph is a press) | The operator, on a phone with a feed's graph on stage: no way back to the roster. Esc did it on a desktop (`nav_pop`), and a phone has no Esc — the same gap the node overlay had on 2026-09-27. BACK now heads the graph's frame, as it heads the node overlay's and the universe's: it pops to the roster beneath, or asks for one when the graph arrived with none (cwd-follow, the dashboard); it is hidden while the roster is what is shown. Law: the-way-out-is-a-press (a node, a graph, an entered feed alike).
| 2026-09-30 (the facts plate stands down from its own ground) | The operator, on a phone, touched the background of the graph to put the node facts away and nothing moved. The plate is 72% black on black at the foot of the picture — on a phone it IS the background to the eye — and the tap landed on the plate, not the canvas, so the scene's empty-space off switch never fired. A tap on the plate's own ground (not on a verb) now stands the plate down and clears the selection, the same as a tap on empty space; a dive's immersed facts are left alone. Verified at 412×915 and on the desktop: tap a node, tap the plate, the plate is gone. Law: a-tap-on-the-readout-is-a-tap-on-the-background.
| 2026-09-30 (a frame says it holds more) | The operator, with the cohort's verbs open in the header band: a grey browser scrollbar down the LCARS frame, and a second one along the foot of the rows. The frame scrolls by design — a band or a short pane cannot grow for its verbs, since gathering never moves the body — but it scrolled with the browser's own bar showing, and the rows' bar was a defect: the indicated row bleeds half a rem under the frame's spine, which the field counted as overflow. The frame's scrollbar is gone and a chip at its foot reads `▼ N MORE` while blocks lie below the view, `▲ TOP` once the foot is reached; the press shows them. It is the band's own `+N MORE` grammar, now on chrome, wired to every frame as it is minted. The field clips the bleed, which lies under opaque chrome. Law: a-frame-says-it-holds-more.
| 2026-09-30 (a pull ends in the bar it would have shown) | The operator's screenshot: four series home with full bars and `✓ N IN CUBE`, the fifth, pulled a moment ago, reading the word `✓ PULLED` and no bar. The wire's done state painted text alone, while a series the answer found already home painted a bar — two drawings of one state. Done now paints the full bar with the count, as the answer does; and a row built after the pull (the cohort rebuilt by SAVE, a listing redrawn) asks the last state the wire reported, so it does not come back reading NOT RETRIEVED. Verified live: a one-file series pulled on a private daemon went idle → RETRIEVING 0/1 → `✓ 1 IN CUBE` with the bar, in the answer and in the band alike, and kept it through SAVE. Law: a-pull-ends-in-the-bar-it-would-have-shown.
| 2026-09-30 (JSON reads as structure) | The cohort manifest opened in the browser as one line of minified bytes. A JSON file is now laid out as the structure it is — two-space indentation, keys in the listing's blue, strings in the light tint, numbers in orange, literals in honey — with RAW on the header for the bytes as written and PRETTY to return; a file that does not parse, or is past 4 MB, is shown as it is. The layout is `features/files/json.ts`, dependency-free like the CSV reader, and unit-tested. Law: a-file-reads-as-what-it-is.
| 2026-09-30 (the band's frame retracts with the panes') | Found by the parity guard: Esc stood every pane's indicated row down and left the cohort row lit in the header band. The band's field hosts its own frame (`data-frame-host`) and had its own strip toggle, while the one retraction handler — Esc, the fill, another strip — read only `.workspace-pane`. Two grammars for one piece of chrome, and the band's was the lesser: no Esc, no fill. The retraction handler now reads the frame host as it reads a pane, the band's private toggle is gone, and the façade's observer (already watching the host) stands the row down as the frame retracts. The smoke's plain state no longer reaches for the strip: Esc clears both listings, and the strip is checked to still open and close the band's frame. Law: one-retraction-grammar-header-and-pane.
| 2026-09-30 (a series looks the same wherever it is listed) | The operator, from three screenshots: the same series in the header band's cohort and in the PACS answer below it looked subtly, needlessly different, and a series pulled to the last file, once indicated, could not be told from one never fetched. Two causes. The cohort listing wore its own index hue (butter), a hue chosen when the number was made chrome; under the 25% dim a butter capsule went grey while the answer's daybreak stayed blue, and a row that is the same series read as another kind of row. It now takes the series listing's hue: the PACS answer is the reference, and the cohort lists ITS rows. And the indicated row inverted its bar whole — a black fill in a black-tinted track — so full and empty were one dark stripe; the well is now solid black and the fill keeps a colour, the listing's light tint when done and the state's own when running or failed, so an empty well is empty and a full one is lit, on both listings, from one façade rule. Verified live on a private daemon: SER001 of the known accession gathered, indicated in each listing, the two rows measured the same (row, capsule, bar) and the pulled bar lit. Neither slip could have been caught: the canon fingerprints exclude listing rows as content, and no gate compared one row in two places. Now one does — smoke stage `gather-series-parity` gathers a fully pulled series, measures the same row in the band and in the answer (row, opacity, index pill, digits, description, badge, bar, note, files) plain, indicated and dimmed, and asserts the indicated bar stands off the idle track (≥ 3:1) and the row; against the stylesheet before this fix all four checks go red. Laws: a-series-looks-the-same-wherever-it-is-listed; an-indicated-bar-keeps-its-colour.
| 2026-09-29 (the band says it holds more) | The operator asked whether the header's share of the screen should be draggable, and whether its panels should wear pane frames with ZOOM for a long cohort. The first already was — the band's foot drags, and every face keeps that one height so gathering never moves the body — and the second was declined: a framed, zoomable header is a second workspace, and the header is the instrument cluster, not a place to work. What was missing was finding either. The band's grip is now visible at rest and takes a finger; and a cohort longer than the band shows a chip at its foot, `+N MORE · ON STAGE`, counting the rows below the view — pressed, the whole cohort is a pane on the main panel (the ON STAGE block's own act, which sat on a retracted frame). Nothing is hidden: the band still scrolls, and the chip leaves when all is in view. |
| 2026-09-27 (a finger resizes the console) | The operator, on a phone: the console's foot would not move. Its access strip (and the header band's) listened for the mouse alone, and a phone sends pointer and touch events. Both drags now follow pointer events with the pointer captured, so a finger, a pen and a mouse drag alike; the strips take `touch-action: none` so the drag is not taken for a scroll; and the console's four-pixel strip keeps its look but takes a touch across an invisible band around it — deeper into the console above than into the lid below, whose toggle keeps most of its height (the lid first painted over the band and took the touch). Measured in phone emulation: a 150 px finger drag grows the console 150 px; a mouse drag still does. |
| 2026-09-27 (the way out of a node is a press) | The operator, on a phone inside a node: no way out — the header read ESC EXITS NODE, a phone has no Esc, and the caption was clipped under the pane's frame; and an elbow hung beside it. The node's view was two framed things stacked: a strip of its own (INSIDE …) over a files browser with its own frame, all under the pane's frame, whose elbow drew above the strip. Asked, the operator chose to drop the strip rather than join it into the frame: the breadcrumb already says where inside the node one is, and the pane's bar says INSIDE FEED. The node's browser now covers the pane's frame, so one frame and one elbow stand; at the node's root the listing's way up reads EXIT NODE and leaves the node (it had walked out of the graph into /proc, or mistaken `feed_<id>` for a hop to a node), and EXIT NODE is the first block on the node's frame beside BACK. | smoke:node-dive
| 2026-09-26 (the first browser waits for nothing) | ORRERY #722 finished: the session lays the universe out itself when its index is whole, every layout, in a worker thread; the first browser, not only the second, draws at once, and one that asks while the session is still at it shows the session's own bar — `LAYING OUT 3,034 SPHERES · THE SESSION · 66%` — then draws (3.6 s from the press on a fresh daemon). Two defects found on the way, both in the surface: a remember timer set under SHAPES that fired after FEEDS was back filed the folded shapes as the galaxy — for the browser and, now, for the session's shared copy; and a return to FEEDS settled the whole space afresh, racing whatever the operator did next, so a cancelled settle left the scene holding the folded shapes and entering a feed re-settled all 3,064 spheres. Places are now kept only for the view on stage, SHAPES keeps its own, a view already seen is drawn where it stood, and only places settled with the physics as they come are given to the session (a knob turned is play). | smoke:universe-pane
| 2026-09-26 (whose work it began from) | ORRERY #717 finished: under DATA a feed that began from another feed's output carries a faint thread from that feed's root to its own — its lineage, read by the index from where its data lies (190 of the operator's 709 feeds) — drawn as a thread at a dim node's strength and never a tube, since it is a relation and not an edge of the run. #716 finished: a series in CUBE's PACS store is described by CUBE's own series record, asked once per patient, before any header is read. | smoke:universe-data
| 2026-09-24 (tubes follow the pull) | The operator, inside a feed: "I clicked and dragged a node and it all exploded..! the cylinders should match the pull etc of the node in force directed logic. Also I think this display with cylinders (and drag reactivity) should be the default DAG rendering too... and for finished feed, the cylinder pulse should cycle"; then "have the dag nodes start a few shades darker and when the pulse hits them they glow bright and stay on .. when the animation recycles they should dim first and rinse repeat". (1) The explosion: the pull plane was built through the hit in the group's local frame and the pointer's world point was fed back as local — off by the group's turn, so the grabbed node leapt and the pull simulation (default forces, not the layout's) threw its molecule apart. The plane now goes through the node's world position, the pointer is brought back into the group's frame, and the pull runs the layout's own forces (links at the sum of the radii, charge by size, collision). (2) Tubes follow: every tube is re-seated on the spheres it joins each time positions sync, so a pulled node drags its tubes and its chain with it. (3) Tubes are the default: drawn as spheres — the DAG pane and the universe's SPHERES — every edge not dimmed is a tube; dimmed scenery keeps its faint lines. (4) A finished feed replays its run in a cycle: the wave goes stage by stage in the order the stages ran, rests, and goes again; each stage starts the cycle dimmed, flares as its pulse lands and stays lit, and the lot dims before the next run. The operator, on the first cut: "doesn't look very dramatic/cinematic. Maybe nodes should be darker .. and when a node lights up a nebula glow aftereffect that the settles down to the lit state would be good. Also when a dag is executing the tube should continuously pulse it and the execution node should light up and off until complete". A stage now rests at under a fifth of its colour; as its pulse lands it flashes toward white and a soft glow blooms out to several of its radii and settles into the lit sphere. A running stage blinks with the stream: each pulse that lands into it blooms and dies back toward dark, until the stage is done — then its tubes are re-read from the statuses as they are and the finished run joins the replay. Found on the way: the feed on stage arriving again with the same shape (a second open, a watch's answer) rebuilt the whole scene — a resettle that dropped the tubes for a moment and restarted every pulse; a same-shaped arrival now patches statuses in place. Found on the way: the cycling shader's new uniform shared its name with a local colour, the tube shader failed to compile and every tube vanished — a shader error is a console line, not a page error, so the probes now listen to the console too. |
| 2026-09-25 (a frame block does what its word says) | The operator, on a phone: "Zoom on a rendered Dicom Pans the display not zoom. You need tests for each frame button". The library's zoom, by default, pinches to zoom and pans on a one-finger drag, so ZOOM under a finger panned; nothing had ever driven a block and read what it did — the smoke read the block's own light. ZOOM now only zooms, by mouse and by finger. `image state` reads the field back (layout, tool, the primary binding, slice, annotations, and per viewport its scale, focus and window), and the smoke drives every block — W/L, ZOOM and PAN by finger and by mouse, the two-finger slice step, LENGTH, ANGLE, PROBE, and ZOOM/PAN on a rendered volume — and checks the field, not the pill. One finding kept: the library drops mouse input for two seconds after a touch and touch after a mouse, so a finger's tap is never read twice; a screen that takes both waits that long between them. | smoke:image-frame
| 2026-09-23 (the space turns about what is looked at) | The operator, after the click fix: they zoomed in, clicked a node, and "the camera zoomed out, paused, then zoomed in somewhere nowhere close to the clicked node … paused … paused … and then suddenly the whole viewport transitioned"; a feed entered "flew away" when the universe resumed rotating — "the camera needs to move/rotate with the universe". Driven again on the real GPU (headless Chromium over ANGLE/EGL on the GTX 980; the earlier simulation ran on software WebGL at under a frame a second, too slow to show timing) and profiled. (1) Every descent or cluster rebuilt the whole scene: the force settle ran over all three thousand spheres though all but a handful were pinned (4.7 s), and a fresh sphere geometry was made per node (0.8 s). Flights are timed by the clock, so a flight caught under that freeze arrived in one frame — the sudden transition. The settle now simulates only what moves (the free nodes and what they link to; a pinned node stands and pushes on nothing), and spheres share geometry by radius: entering a feed went from about 10 s to about 1.2 s, unfolding a shape takes 60 ms. (2) The idle spin, the orbit and the wheel turned about the world's origin while the camera, after a flight, looked at the feed: the spin swung the feed out of view and the wheel slid past it. The scene now keeps a FOCUS — the point the camera looks at, set by a fit and by every flight — and the spin and orbit turn the space about it, the wheel dollies toward it, a pan carries it. An entered feed turns in place in the middle of the frame. (3) A click that wobbled more than four pixels on a sphere became a pull, and a pull re-simulated all three thousand nodes under default physics (none of the universe's reach or anchors): the field reshaped and the click was swallowed. A pull now moves the grabbed node's own molecule, never across a halo, never a dimmed node. (4) Inside a feed a dimmed cluster halo still took the pointer; it is scenery like a dimmed sphere. Still the kernel's: `feed diagram` answers in about a second on a settled daemon and about eight on one just booted, shown as ENTERING with ASKING. Found in passing, not this change: the `mode-frame` smoke fails its two resting-frame checks on main too. |
| 2026-09-23 (a click does not resettle the space) | The operator, on the built universe: "click on a node — the universe zoomed out totally, paused, and then the zoom into node animation happened without even showing what node"; inside a feed "clicking it sometimes showed the overlay, sometimes didn't"; "I could never double click from there to enter a node"; "the whole universe would reset". Simulated with a pointer on a private daemon, four causes, each read off the code and confirmed in the simulation. (1) A SELECT rebuilt the scene: every click re-settled all three thousand spheres and refit the camera before the descent flew — the zoom-out and the pause. Selection now only paints the emissive on the meshes it has (`selection_paint`); a click freezes for 11 ms, not 31 ms plus a settle. (2) Inside a feed the dimmed field behind it still caught the pointer: a hover named a background sphere, a click on it did nothing (it belongs to no entered node) and a double click on it flew nowhere — "sometimes". A dimmed node is scenery and takes no pick (`userData.dim`, filtered before the halo rule). (3) A space re-arriving (the lab moving re-asks `proc universe`; a landing repaints) framed the whole space again, throwing away wherever the operator had wheeled, and could do so under a descent in flight — "reset to a different selection of settings and then revert". A repaint now keeps a camera the operator has placed (`camera_touched`: a wheel or a drag since the scene last framed the graph; a fresh space is still framed whole) and is held, like a landing while inside, from the click until the feed is entered. (4) The ask was silent: nothing on the pane said a click had happened while `feed diagram` ran (seconds on a large feed), so a slow answer read as a dead click. The bar now reads `UNIVERSE — ENTERING FEED n · title …` with the pill ASKING until the graph arrives, and an ask that yields nothing writes a line to the console saying which feed and after how long. Framing inside a giant feed: the camera frames the nearest four fifths of the nodes (`camera_flyToFit … bulk`), not the outliers, so a node is wide enough to hover; the wheel reaches the rest. Verified by the simulation: default universe on stage; sphere click → ENTERING → INSIDE; node click shows the facts; double click opens the node's overlay inside; Esc closes it and the feed stands as it was. Law: a-click-does-not-resettle-the-space — indicating a node changes only the node's paint; nothing an operator has placed (camera, settle) moves because they pressed. |
| 2026-09-23 (enter node flies in) | The operator, inside a feed in the universe: "neither the ENTER NODE pill nor clicking it enters the node". It did what the DAG pane's drawer verb of that name does — moved the session's cwd — which is invisible from a field, and the words said something else. Inside the universe ENTER NODE now flies in: the camera dollies into the sphere and the node's data opens inside it as a rooted browser (the DAG pane's overlay, shared: it looks for either pane's field and asks either pane to fly back), Esc flies out, a double click on the node does the same, and `universe node <instance>` does it by word. The overlay's immersive hop — clicking a descendant instance inside a node flies to its node — works in the universe too. Esc inside a node belongs to the overlay; the pane's own Esc (climb out of the feed) waits until the camera is back. Noted, not done: the DAG pane's drawer verb is still named ENTER NODE for a cd, and the same words should mean the same thing on both panes. |
| 2026-09-23 (every job in the universe) | The operator: "and to see more of a universe of many nodes?" The universe's resolution was the stage: a fan of three hundred runs one sphere with `×300`. Beneath that is the job, and the DAG pane already had the projection for it — CENSUS, every member of a `×N` node as a point shelled around it in one instanced mesh, which held 88,000 live. The counts ride the universe's groups, so the same projection costs the universe no data and no call: a DENSITY block on the frame, SHAPE or CENSUS, and `universe density shape|census` by word. Two things the scene needed for it: a ghost (a cluster's anchor, drawn as a halo) carries the feeds it gathers as a count and must not be shelled as jobs — a census of it drew a shell of nothing; and the hover tip picked only meshes, of which a census has none, so in census the tip names the group beneath the pointer the way a click already did. On the operator's CUBE that is 210,012 points, each in its molecule, each molecule in its cluster: the whole of what the lab has computed, at the resolution of the job. |
| 2026-09-22 (the universe has physics knobs) | The operator, on the universe pane's frame beside the RUNS pane's: "no gravity controls etc — is that expected?" It was: the universe used to be drawn in the RUNS pane and wore that pane's modes, most of which mean nothing for a space of feeds (no pulse to replay, no census, no compute hue); the universe pane wears its own. But the physics knobs were play the operator had, and play is worth a block: GRAVITY ON/OFF on the frame, and `universe physics charge|link|collide|gravity on|off` and `universe physics reset` by word, the same terms the DAG pane's `dag physics` turns. Refused while inside a feed or a cluster, whose settle is the descent's own. The pane's reach (the hug) rides on top of the operator's terms and is not theirs to turn. |
| 2026-09-22 (a shape folds its feeds) | The operator, testing the vocabulary: "imagine I have thousands of dags of the same bespoke pipeline. What's my universe? One node?" It was a thousand molecules under one halo — honest, and a blob. The fold across feeds was built: one molecule per pipeline shape, its spheres the shape's stages, each sized by the feeds folded under it and hued by how many of those feeds erred there — a thousand feeds of one pipeline as one molecule, `×1,000`; a click unfolds it in place into its members (the feed descent's settle, one level up). Then, on seeing it: "won't that collapse things more? I want a wow factor of all the compute structure in my universe." Yes — folding is compression, and compression is not the default. So the fold is a VIEW: FEEDS (every feed its own molecule, the whole structure) is the top of the universe, SHAPES folds it, a block on the frame and `universe view feeds|shapes` by word. In FEEDS a cluster's halo lights its feeds where they stand; in SHAPES the folded molecule unfolds. The shape string is the fold's key, so nothing changes in the kernel and no call is added. The scale block's second word became ALIKE (every sphere alike), because a second FEEDS on the frame would have meant two things. Vocabulary, restated for the record: a NODE is a subset of one feed's jobs (one plugin under one parent stage, by the kernel's fold rule); a MOLECULE is one feed; a SHAPE is what feeds fold under; nothing ever joins two feeds by an edge. |
| 2026-09-22 (the universe takes the stage) | The operator, first look at the published universe: the tile split it beside a file browser, and once the browser was closed and the frame opened, "most of the right of the frame is offscreen". Two faults. The stage: slice 1 spawned the universe beside the errand host, the way an image pane opens beside the row that asked for it — but nothing asked for the universe beside anything; it is a domain, like RUNS and PACS, and reads as a fragment when it shares a stage. It is now a primary pane with a preset of its own (`universe`), entered whole from the tile. The frame: the rule that widens `--spine-w` to the bar's width when a field's frame opens names each field by class, and the universe field was not among them, so its frame opened at strip width (21px) and its blocks spilled off the right of the pane — the one shot that would have shown it, the frame open on a lone universe, was never taken. `.universe-canvas` joins the rule; the probe now measures the open frame's right edge against the viewport. A third, found by the smoke on the way: the orphan sweep that runs on every domain change disposes any off-stage pane that is not a primary by name, and the new primary was not on its list, so leaving for the dashboard destroyed the universe and the tile's second press raised nothing. Lesson, again: a layout change is looked at in every state it has, and a field that borrows a frame borrows every rule the frame has; a primary is a primary everywhere the word is spelled out. |
| 2026-09-22 (a cluster has a handle) | Slice 3 of the universe descent, the last on the plan. The cluster — the feeds of one pipeline shape, gathered by an unseen anchor — had no handle: nothing to hover, nothing to press, so "show me all the FastSurfer runs" had no answer on the field. The anchor is now drawn as a HALO: a translucent sphere at its place, sized by the feeds it gathers (`haloRadius_compute`, a square root so a crowd's halo grows without swallowing its neighbour), unlit and written last so the molecules show through. Hover names it: `pl-dircopy > pl-dcm2niix > pl-fastsurfer · 12 feeds`. A click brings the cluster into view: every feed of the shape lit where it stands, the rest dimmed, nothing moved (the whole field frozen through that settle), the camera flown to frame it — the fsv reading one level up from a feed, a directory of feeds alike; BACK, Esc or `universe back` returns, and a feed entered from a cluster climbs back to the cluster, not past it. The pick rule that makes a halo bearable: a halo wraps its cluster, so its surface is hit before the spheres inside it, and a solid hit anywhere along the ray wins; the halo is picked only over nothing solid. `universe cluster <feed>` names a cluster by any feed in it, since a shape has no name a person would type. Not done, noted for later: a listing of the cluster's feeds beside the field (the RUNS roster cannot filter by shape yet), and PROCESS-this-shape. |
| 2026-09-22 (the descent into a feed) | Slice 2 of the universe descent, the three decisions as grilled. The operator: the fly-in "feels more fsv like and kinda cool" — the universe as the filesystem seen from above, descending into a molecule as into a directory. A click on a sphere flies the camera to its molecule (`camera_flyToFit`, a dolly along the line the camera already looks down, never a swing), asks the kernel for the feed's graph — `feed diagram`, the same the RUNS pane draws, silently — and unfolds the molecule into it IN PLACE: every new node starts at the molecule's centre and settles out from it (the seed), every other feed dims to a sixth of its opacity with its edges (`SceneNode.dim`), the anchors stand, and the camera frames the feed. (1) The BUDGET is the kernel's own collapse: a fan is one `×N` node with a tally, so an 80,000-job feed unfolds to its shape, not to 80,000 spheres; the facts carry the true count and the errored share hues the sphere as it does outside. (2) The universe is its own pane, so the descent keeps the space's camera, memory and landings; a space re-arriving or a landing while inside is taken and drawn on the climb, never under the operator's feet. (3) Inside, a node offers what the DAG pane's node offers and nothing invented: its facts (PLUGIN, INSTANCE, STATUS, COUNT, DATA) and the verbs ENTER NODE and PROCESS on the overlay, plus OPEN FEED on the frame into a RUNS pane of its own; Esc, BACK on the frame, or `universe back` climbs out. The console subject `universe` (`enter <feed>`, `back`, `open`) is the surface's — the kernel has no `universe` command; `proc universe` is what the pane asks — and is how the smoke drives the descent, since a pointer over a settling sphere is not something a smoke can aim. A graph that arrives under a flight keeps the camera where the flight put it (`graph_set … fit: false`); the earlier `rebuild` framed every arrival and would have snapped the descent flat. Three things dragged the unfolding feed a hundred units from where it stood before the settle was made honest, each read off an instrumented flight: a root linked to its shape anchor at full strength (d3's link strength is one over the lesser degree, and a root's is one) was hauled across the cluster, so an entered feed keeps its place by its seed and links to nothing; gravity pulls to the origin, so the descent's settle runs without it; and d3's centering force shifts every free node by the mean of ALL nodes each tick, so a settle that freezes the field (every other node pinned, pushing on nothing — `graph_set … frozen`, physics for that settle alone) drops centering too. Framed radius: 63 before, 20 after. |
| 2026-09-22 (a sphere weighs its jobs honestly) | The operator, on their own universe: "dominated by errors in only one or two feeds (like the massive errors in the 80k node)" and, of the older build, "a fat red and blue caterpillar". Two encodings were the cause, not the data. SIZE was linear in jobs, so an 80,000-job group was the peak, every other group sat at the smallest radius, and gravity (mass-weighted) parked the giant at the heart. A sphere's radius is now the LOG of its jobs (`jobsMetric_compute`): eighty thousand reads as a few times three hundred, not two hundred and sixty. COLOUR was the worst status, so one failed job in eighty thousand painted the whole group red. A sphere is now hued by the SHARE of its jobs that failed — done shading to error by the square root of the share, so a trace of failures still shows as a trace, red outright only when all failed — which needs one number the kernel did not send: `errored` beside `count` on a job group (cumin `pluginGroups_build`, menu, defaulting to zero from an older daemon). The tip says the count too: `pl-dcm2niix ×80,000 · 12 errored`. A SCALE block joins the frame: JOBS (the log) or FEEDS (every sphere alike; the field reads by shape alone), the way the DAG pane offers TIME and SIZE. Law: an encoding must not let one datum erase the rest — the whole point of a field is the many. |
| 2026-09-22 (the universe is its own pane) | Slice 1 of the universe descent, grilled with the operator (plan: universe-descent). Three vocabulary facts first, because the gestures hang on them: a SPHERE is one plugin group inside one feed, a MOLECULE is one feed, a CLUSTER is the feeds of one shape gathered by an unseen anchor — two of the three are drawn. Decisions taken: (1) the UNIVERSE is its own pane kind, `UniversePanel` on the shared scene, opened from the dashboard's tile beside the errand host and focused rather than duplicated on a second press; RUNS-02 stays a feed viewer (the wait's theater it draws while the roster is refused shares the graph and the tip, and is not this pane). (2) Hover names both what a sphere is and whose it is: `pl-dcm2niix ×300 · finishedWithError · feed 4299 · shot-cohort`, through a `tip` handler the scene now takes, since a sphere's identity should not cost a click; the feed's title rides the landing for it (cumin, menu, brasa). (3) The space BREATHES: landings keep arriving after the index is whole (a visit delta adds instances, and a landing is what that is), and a feed arriving through the roster delta or the feed count moving re-asks `proc universe` (cache-resident, at most once in five seconds), so a new feed drifts in and a gone one leaves. (4) It HUGS: a bounded repulsion reach (`PhysicsTerms.reach`, 12 units) lets one molecule hug itself instead of spreading across the field as six dots at the first landing, while a crowd still spreads. (5) It REMEMBERS: where the molecules settled is written per identity (`argus.universe.<user>@<uri>`) a few seconds after each settle and seeds the next, so a reopened pane or a reloaded page finds them where they were. Not in this slice: the pane does not survive a desktop's dormancy (its replay intent is the descent's business), the cluster has no handle yet, and a click on a sphere does nothing until slice 2 flies in. Reference: components.adoc, Universe pane. |
| 2026-09-22 (a feed lands from its leaves) | The operator, through the door at ten percent of a cold sweep: "the universe is not showing anything". Reproduced on a cold private daemon: the UNIVERSE titled `1 FEEDS · 1 SHAPES` while the canvas stayed black, and a debug read of the scene said zero nodes — the one landed feed carried no groups. The cause was in the index, not the surface: CUBE lists plugin instances newest first, so a cold sweep meets a feed's LEAVES long before its ROOT, and the landing shape was walked down from roots — empty for the whole of a 4,599-job feed until its oldest instance arrived, which for the newest feeds is the end of their block and for the oldest feeds the end of the sweep. The cache now keeps each feed's roll (every instance it holds) beside its topology, and the landing shape reads the roll: an instance whose parent has not landed stands as a root of its own until it has, and the shape settles as the parents arrive. Verified live: the first feed took its shape at 400 of 210,012 jobs — six nodes on the canvas where there had been none. Two lessons: a surface that says one thing (a feed) and draws another (nothing) is reporting a defect beneath it, and a check that copies a warm checkpoint never exercises the cold path — the earlier verification of the universe did exactly that. |
| 2026-09-22 (a session greets its operator) | The operator, on the console's first lines: "two projections of one CALYPSO session: type below, watch the instruments" is "word slop"; show the ChRIS splash, then what a terminal on access should say — "Welcome to ARGUS, <name>. You have X feeds and XX jobs completed, with an x% job failure rate". Four decisions, all taken as recommended. (1) The greeting is the SESSION's, so it is a kernel command, `motd [surface]`, with a typed `session.motd` model beneath the text: a TTY and this console ask the same command and say the same facts (`surface-never-shadows-the-session`, `image-is-a-kernel-command`); the surface passes only its own name, since the kernel does not know its surfaces. (2) The splash is the brain at REST, `@fnndsc/menu/logo`'s static end state — the art the TTY boot and the porter's greeter already draw — not animated: through the door the greeter just animated it, and theater twice is theater. (3) The greeting is asked at attach and written with what the index holds so far — "4,700 of 210,012 jobs indexed so far", or "your index is still cold" — never reprinted later; waiting for the sweep would leave a blank console for the length of a cold boot, and the INDEX instrument carries the rest. (4) The fortune the TTY boot always showed rides inside the motd, so both surfaces end on the same cookie. The numbers cost no call: the session's own feed roster (feeds by scope, jobs by state, failure = errored over settled). The first build wrote the brain as twenty-five transcript lines and the shot showed no brain at all: the console shows about a dozen rows, so the art had scrolled itself out of sight before the greeting arrived — a splash nobody saw. The splash is now one block of two columns, the brain drawn at under half the transcript's size (eleven lines tall, `white-space: pre` so its columns hold) and the greeting beside it, the whole of it sized to the console's visible rows; the greeting lands a moment after the splash in its own column, because the prompt is its own bar. An older daemon without `motd`, or a refused ask, leaves the brain alone rather than an error. Verified live: brain 243px in a 255px console, greeting `728 feeds · 210,012 jobs run · 13.6 % failed · 0 running, 90 scheduled` beside it. |
| 2026-09-21 (a table's frame is its own) | Operator catch, two shots of a CSV on stage: the closed spine climbing up into the path header over CLOSE, with no elbow; and the opened frame a bare slab — "it also carries NO VERBS, at the very least it should carry FILTER". Measured: the field sat at y=1046, 383px tall; its rule, strip, elbow and frame at y=44, 1350px tall — drawn against the PANE. The pane's body carries `--mode-frame-top` for its own frame (the offset down to its path header), and the table's field, a descendant, inherited it. A field owns its frame's top: the table field sets the variable to zero, and its chrome sits where the field is. The frame did carry FILTER — one block, OFF and dim, above a bright fill, which reads as nothing. A table's verbs live on its frame as every field's do: FILTER for the rows, RAW for the bytes as written, CLOSE to leave the table; the header keeps the path and the state and no pills. Law standing: a-control-lives-where-it-acts; the corollary: a-field-owns-its-frame — a field's chrome is positioned by the field, never by a variable meant for the pane around it. |
| 2026-09-25 (a download saves the file or says why not) | The operator, in a browser: "Hitting download on a file in cube (like a csv export that is in CFS) triggers the download in browser but for some reason the download that comes seems broken and when lands on my computer is called 'vfs'". Traced end to end, the daemon and the door both answer that CSV correctly — a 200, an attachment, its own name. A file called `vfs` is what the browser saves when the answer is NOT the file: DOWNLOAD handed the byte route to the browser to fetch on its own, and it saved whatever came back — the door's login page after a lapsed cookie, the daemon's `not found` — under the route's last segment. Now the page asks first with its own credentials and saves only an answer that is the file (2xx, not redirected, carrying the attachment header); the name is the header's, else the path's, never the route's; anything else saves nothing and the console says why (`download: <path>: the session could not read that file — nothing was saved`). A success says so too (`download: <name>`). A file over 256 MB is handed back to the browser to stream once its first answer has proved it is the file. | smoke:file-download
| 2026-09-21 (a file comes down as a download) | The operator: "for individual files, add in ARGUS a download that triggers the browser 'download' operation". A DOWNLOAD verb already stood on every file row — and opened the bytes in a new tab, where a browser SHOWS a CSV or a PNG rather than saving it; only a file the browser could not display ever reached the disk. The daemon's `/vfs` route now takes `download=1` and answers the same bytes as an attachment named for the file (`content-disposition`, the name carried as ASCII and as UTF-8 per RFC 6266), and the surface presses that URL through an anchor's `download`, which is the browser's own save gesture: the file lands under its own name, nothing opens. Verified live: the row verb saved the CSV on stage; through the porter the header travels the proxy untouched. The verb also stands where the file is on stage: DOWNLOAD beside CLOSE on a text file's and an image's header, so a file being READ can be taken without going back to its row (`a-control-lives-where-it-acts`); a CSV opened as a table takes it on its frame once the frame carries its verbs (#657). The kernel's `download <chris_path> <local_path>` is the terminal's projection of the same act — its disk is the engine's host — and the browser's projection writes the only disk a browser has. |
| 2026-09-21 (a patient answers to a number) | Operator catch: a query for two patients showed `1` beside each — "this should be PAT001 PAT002 I'd venture". It should: the patient level had no KIND, so under a-row-wears-the-number-it-answers-to it counted itself dim, per group, which read as a number. A patient is a kind now, in its own sequence, and hands a verb every series of every study of theirs — `gather add @PAT001` gathers a patient, as the row's GATHER would. A patient has no path of its own, only its series' paths, so the address a surface matches on is minted from what names it (the identifier as asked, the PACS asked) and lives in the wire package, so the kernel and the pane spell it once. A patient the PACS answered nothing for wears no number: it has nothing to hand, and a lit number that hands nothing is a control that cannot act. |
| 2026-09-21 (the space of everything run here) | The operator, watching a cold session warm: could the wait show a growing DAG shaped by gravity, and could it encode the warming space itself — "a massive uber-DAG, each new DAG materialising in real time"? Yes, and honestly. The session already knows which feeds' topology the index just read, and now says what they are on the prompt it pushes once a second: each feed's jobs COLLAPSED by plugin per place in its pipeline — a chain is a chain, a fan of three hundred conversions is one node of 300 — with a status on every node. The RUNS canvas draws them as they land: every feed its own small DAG, floating free like molecules in a solution, counts as weight and status as hue, and likeness pulling like together — feeds of one pipeline shape hang from one unseen anchor (a ghost node: in the settle, never drawn), so the space organises itself into constellations of "things run this way". A first cut drew feeds as single dots on a tree of plugins; the operator saw 7,500 jobs read and a dozen dots and said so — the number was honest and the picture was dull. Every job as a node would not be a picture at all (210,012 in a force layout); collapsing keeps the SHAPE and loses only the count, which the node carries. Every node is real — a group of jobs that ran — which is what makes it a readout and not a screensaver. Nodes carry no feed label and answer no click yet (parked by the operator with the pane). Tied to the warm-up: it begins when the roster is refused for warming, grows with each landing, and yields to the roster the moment the index is whole. The same picture is a face: `proc universe` answers it from the cache at any time and says whether the index is whole, and the dashboard's UNIVERSE tile puts it on the RUNS canvas. Law: a-wait-is-drawn-with-what-is-known — a surface that must wait shows what the session already holds, never something made up to fill the time. |
| 2026-09-21 (a figure that moves is the only figure) | Operator catch, on a cold session: the header and the console counted the index warming up correctly, while RUNS-02 stood on "the visible-job index is still warming (300/210012, 0%)". The pane already had a live figure beneath the refusal, moving with the prompt context — but the refusal line above it repeated the count as it stood at the instant of refusal, and a number that never moves beside one that does reads as the truth. The refusal keeps its words and loses its count (`dag/refusal.ts`); the live line is the one figure. Law standing: a-readout-that-acts-says-so; the corollary written here: a-figure-that-moves-is-the-only-figure — a surface never shows a frozen copy of a number it is also showing live. |
| 2026-09-21 (a surface leaves by the door it came in) | The porter lets a browser in with a cookie and mounts its session at `/s/<key>/`; the browser holds no token and asked for none. Leaving has to be as plain: a LOG OUT pill beside AUDIO and the theme, in the frame's own hue because leaving is chrome and not a state, standing ONLY on a page that came through a door (`?door`) — at a daemon's root there is no door to leave by and the pill is not drawn. Pressed, it tells the door to forget this browser (`POST …/logout`) and goes to the door's login. The session is not touched: a daemon outlives every surface, the door's own policy (idle eviction) decides when it ends, and a LOG OUT that killed the session would make "log in again" a cold boot for a colleague sharing the identity. The door's routes are found from the page's own address — two directories above the mount — so a front that puts the whole porter under a prefix moves them too. Law: a-surface-leaves-by-the-door-it-came-in — a surface that was let in by a door offers to leave by that door and by nothing else. |
| 2026-09-21 (a surface under a prefix) | The porter — the display manager, gdm to argus's DE, settled this day — proxies one identity's daemon under `/s/<identity>/` on one origin over TLS, and the browser holding an attach token in its URL bar ends with it: the door holds the token and puts it on the proxied attach. Two things in the page stood in the way, both absolute: the wire at `ws://<host>` (root, and `ws:` on an `https:` page is refused) and the byte route at `/vfs?…&token=`. Every address is now derived from where the page was served (`calypso/routes.ts`, proved under jest with a plain location): the page's directory is the daemon's mount, the socket scheme follows the page's, `vfs?…` is relative and carries a token only when the page holds one. A page sent through a door arrives at `…/?door` and attaches at once with an empty token rather than showing the attach form for something it is never told. At the daemon's root nothing changes, and the `?ws=` dev override stands. Law: a-surface-knows-its-address-from-its-page — a surface derives where its session is from where it was served, never from a root it assumes. |
| 2026-09-20 (a lid breathes at its end block) | Operator catch: with the console closed, a block at the left end of its lid pulsed against DASHBOARD-01 and the gutter seemed to echo it. Two rules collided. The lid's own rule says the beckon lives on the END BLOCK only, and says why: "a filter across the whole bar visibly detaches its first segment from the static elbow arm it joins (on medical the seam pulsed on a live display)". The later rule that makes every pressable block breathe had put `lcars-breath` on `#drawer-toggle` — which IS the whole bar row, bars 6-10 at full width — so the first segment pulsed against the static elbow and bar-10 carried two animations at once. `#drawer-toggle` is out of the whole-row breath; `lid-beckon` on bar-10 while closed is the lid's whole pulse, and the reduced-motion rule names that instead. Law, already stated in the stylesheet and now here: a bar that joins a static frame member never filters as a whole — its beckon lives on its end block.
| 2026-09-20 (a pull is not a gather) | Operator catch: pulling a series put it in the cohort. Not a recent break — the PACS pane's founding model (2026-08-28) made pull the selection gesture, "pulling series into a gather, a cohort under construction", when the gather was the pane's own tray; the study slice of 2026-09-18 wrote it down as "pulling gathers what it pulls as it goes". Once the cohort became the session's — the header band, and the kernel's own `gather` — a fetch that also chose was a cross-coupling: PULL means bring it home, GATHER means I chose this, and a series can be either without the other. Series PULL and PULL STUDY now queue their badges and touch the cohort not at all; the cohort is built only by GATHER — a row's, a study's, the table's, or the kernel verb's. Law: a-pull-is-not-a-gather — a fetch never selects.
| 2026-09-20 (CLEAR asks before it throws away) | The operator, settling CLEAR: "once cleared it's gone — if they haven't saved, CALYPSO could ask if they want to save". So CLEAR on a cohort that has changed since it was last SAVEd asks on the band, where the press was — "Save the cohort first?" — YES / NO / ABANDON, keys y, n, Esc. YES saves and then clears; a name abandoned inside that save abandons the clear too, since nothing is thrown away on the way to a save that did not happen. NO clears. Esc leaves everything standing. A cohort already saved clears without a word. The session's working file does not count as a save — it is what lets the cohort outlive a reload — so a cohort never named is unsaved, and a restored one is asked about like any other. Law: a-destructive-act-asks-once — once, on the face it acts on, never twice and never in a console the operator may have closed.
| 2026-09-20 (CLEAR is a verb, and a dismissed header leaves whole) | Operator catch, two faults in one screenshot. First: "there is no button that clears the gather". There was — the frame's whole-cohort block read REMOVE N, the same word as the per-row REMOVE, so it did not read as the thing that empties. It is CLEAR N now, the kernel's own word, and it lowers to a visible `gather clear` — the operator who read it on the frame can type it at a console, which is the levelling AEGIS asks for. DISMISS was the nearest thing and does something else: it sends the band's face away and forgets nothing, and its comments no longer claim otherwise. Second: DISMISS left ghosts — an elbow, a bar, a clipped title at the top of the screen. The header slid away by `--zoom-header-height`, its last RESTED extent, and a face that had just grown the band was taller than that; the header moved by the old number and its lower rows stayed on stage. It glides by `translate: 0 -100%` now, its own box, so it leaves whole whatever its height was, and the measured height serves zoom alone. Law: a-control-says-what-the-kernel-calls-it — a frame block that lowers to a verb wears that verb's name.
| 2026-09-20 (a field fills its pane) | Operator catch: a PACS answer stopped a third of the way down a full-height pane, rows cut mid-line, black beneath. The cause was `.pacs-listing { max-height: 34vh }` — a cap on the VIEWPORT, from the day the PACS workspace stood in the header row above a console and had to leave it room. Once PACS became a body tile the cap outlived its reason and nothing measured the pane it stood in. The field now takes whatever its pane leaves it (`flex: 1 1 0; min-height: 0`, the workspace a flex column at full height) and scrolls within that, as `.listing-field` already did for every other tabular pane. Measured live: the field's bottom sits 13px above the pane's, the fourteenth series row lies past it and the field scrolls to it. Law already standing: a-readout-that-acts-says-so is not this; the nearest rule is that a pane's field is bounded by the pane, never by the screen.
| 2026-09-20 (a file can be gathered) | GATHER was a place-only verb in the browser, on the belief that a run is given a directory and a gathered file would be a member no run could take. The belief was checked against CUBE's source rather than repeated: `pl-dircopy` copies nothing — its `run()` prints a banner — and its `--dir` is CUBE's `unextpath`, whose validation checks only that the caller may READ each comma-separated path and whose two runtime paths each handle a file explicitly (a `ChrisLinkFile` in link mode; the parent's listing narrowed to the one object in in-network mode). Proved live: three files handed to dircopy became three link files at the feed's root, 179 bytes each, no copy. So a file wears GATHER now, and "three and only three files" is a cohort a run can be given. The cohort's own refusal of a mixed feed stands for one more slice — the kernel has no verb yet that hands a path LIST to dircopy — and its note now says so instead of claiming dircopy takes one directory. Still not offered on a projection: a gathered view of /proc is a member nothing could hand over.
| 2026-09-20 (a row wears the number it answers to) | A row of the session's last answer can be named by the number beside it — `gather add @2,3,6`, `image @1` — so the number is DRAWN beside it: the listing façade mints a leading index pill for any level that says how its rows are addressed, and no pane draws a number itself. What is numbered is the kernel's ANSWER, not a rendering of it, so a console table and a pane count the same rows; the pill is found BY ADDRESS rather than by counting down the screen, so a sorted or filtered listing keeps each row's own number. Every listing leads with the column and every row counts itself, but only the rows the kernel reaches wear a LIT number: in a PACS answer that is the SERIES, while the patient and study levels above count themselves dim. The distinction is the hue, never the digits, so a number nothing answers to cannot be mistaken for one that does. Numbers are zero-padded to the widest in their group (a 14-series answer reads 01…14), and the lit hue is the pane's FRAME hue — `--listing-index-hue`, set per pane — because the pill is chrome rather than content: in the browser an orange pill beside an orange kind glyph read as one blob and the number stopped being a number. A row the numbers do not reach keeps the column and shows nothing, so the geometry never moves under the operator — the bargain the action track already makes. The surface learns which listing is numbered from a retained `numbered` message (the kernel announces it as the answer changes; a late attacher is told on attach), the same shape regard already travels in. Law: a-row-wears-the-number-it-answers-to. Verified live against a private daemon: a home listing reads 01…22 in the browser's orange with `..` blank, and a PACS answer reads 01…14 on its series in the answer's own blue, the patient and study rows counting themselves dim.
| 2026-09-20 (a row wears the number it answers to) | A row of the session's last answer can be named by the handle beside it — `gather add @SER2,3,6`, `image @SER1`, `gather add @STD001` — so the handle is DRAWN beside it: the listing façade mints a leading index pill for any level that says how its rows are addressed, and no pane draws a number itself. What is numbered is the kernel's ANSWER, not a rendering of it, so a console table and a pane count the same rows; the pill is found BY ADDRESS rather than by counting down the screen, so a sorted or filtered listing keeps each row's own number. Every listing leads with the column and every row counts itself, but only the rows the kernel reaches wear a LIT number: in a PACS answer that is the SERIES, while the patient and study levels above count themselves dim. The distinction is the hue, never the digits, so a number nothing answers to cannot be mistaken for one that does. A handle says WHAT it counts: a bare number was refused in the grill, because in a PACS answer every study read "1" and a number that names no kind cannot be typed back with any confidence. One sequence per kind across the answer (`STD001`, `SER001…014`), padded to three or to the sequence's width; a study hands a verb its series, as the row's own GATHER does; and a verb refuses a kind it does not take by name at expansion, so `image @STD001` says what it takes and what it was given. Rows the session does not reach count themselves dim, and the lit hue is the pane's FRAME hue — `--listing-index-hue`, set per pane — because the pill is chrome rather than content: in the browser an orange pill beside an orange kind glyph read as one blob and the number stopped being a number. A row the numbers do not reach keeps the column and shows nothing, so the geometry never moves under the operator — the bargain the action track already makes. The surface learns which listing is numbered from a retained `numbered` message (the kernel announces it as the answer changes; a late attacher is told on attach), the same shape regard already travels in. Law: a-row-wears-the-number-it-answers-to. Verified live against a private daemon: a home listing reads 01…22 in the browser's orange with `..` blank, and a PACS answer reads 01…14 on its series in the answer's own blue, the patient and study rows counting themselves dim.
| 2026-09-18 (a frame opens outside a pane) | The operator: "clicking on any series did select the row but the frame did not open." The façade opens a frame by writing `data-modes` on the pane the zone stands in — and the cohort's listing rides the HEADER BAND, which is not a pane at all. So the zone filled with the row's verbs behind a frame nothing had told to open: the row lit and the verbs stayed dark, which is the same fault as the node dive's in a different disguise, and the same lesson twice over. A frame's host is now whatever declares itself one (`data-frame-host`), and a pane is the common case rather than the only one. Same report, the verbs: PROCESS is offered on EVERY member, because it is what a cohort is for and a verb that appears only on the rows that happen to be home reads as a listing changing its mind; pressed on one not yet in CUBE it refuses by name and says to PULL first. PULL stands on the members that are not home. And the frame — which is the whole listing's — carries the cohort's PULL, REMOVE and PROCESS, so a row's verbs act on that row and the frame's act on the set. |
| 2026-09-18 (a member has a kind) | Slices 3 and 4 of the gather rework, which the operator asked for in one line: "in reality the gather is a system wide collection — I can gather PACS series AND other stuff all for one purpose." A member now has a KIND — a series, a directory, a file — and GATHER stands on a browser's directory rows beside PROCESS, so a cohort holds the places a session is working on as well as the series it is fetching. The kind is what keeps the verbs honest, which is the whole reason for having one: PULL is offered only on a series that is not home, because a place is already in ChRIS and offering to fetch it is a promise nothing can keep; IMAGE only on imagery, so a folder of tables is not offered a viewer; PROCESS on anything with a place. The cohort's own bar counts what it actually holds (`1 SERIES · 1 PLACE`) rather than calling everything a series. And CREATE FEED refuses a MIXED cohort by name: a feed is rooted by PULLING its members, a place cannot be pulled, and the kernel roots a feed on one directory rather than several — so it says that, and says what to do instead, rather than rooting a feed on the series and quietly leaving the places out. That refusal is the honest placeholder for the kernel question of rooting one feed on several places. |
| 2026-09-18 (a cohort can take the stage) | Slice 2 of the gather rework. A band is right for reading and curating a cohort, and wrong for two hundred members with a filter on — so the band's frame carries ON STAGE, which puts the cohort on the main panel as a pane and retracts the band, having done its job. What makes it a second VIEW rather than a second cohort is that the set moved out of the panel: `Cohort` holds the members, the name and the feed, and tells whoever is showing it when it changed. Two panels can render one set, so removing a member in the pane empties the row in the band without either telling the other — the same rule the badge registry follows for a series shown in two places, and the answer to the operator's own framing that a gather is a reflection of one truth. A view that owned its members would make the other a copy, and a copy drifts. |
| 2026-09-18 (a cohort row offers the fetch) | The operator: "in the cohort frame, on an unretrieved PACS series, there was no option to PULL — that seems contextual omission." It was. The cohort's rows were written when a cohort held only series already home, so they offered REMOVE, and IMAGE and PROCESS once CUBE named the folder, and nothing for a member that had not been fetched — while the frame above them pulled the whole set. A row is where the operator is looking at THAT member, so it carries the same act for itself: PULL is offered on a member that is not home and carries the path a pull takes, and lowers to the one line the operator could have typed. Same round: the cohort's face was a flex item in a row that packs to the right, which suits a block of readouts and stranded a LISTING in the last third of the band, columns crushed while two thirds sat empty. A listing takes the field it is given. |
| 2026-09-18 (a block joins a column, it is not appended to one) | The operator, on the cohort's new block: "the elbow and frame and colour are contiguous with the CALYPSO colour, and gather is a different thing. So GATHER between ARGUS and CALYPSO." Right. The last block of that column is not merely last: it carries the frame's elbow and its ground RUNS ON into the frame sweeping away beneath it, so appending a block after it cut the corner off the shape and left the new one wearing a radius that belongs to the frame rather than to it. The cohort's block sits between the two instead, takes the light seat between the two dark ones since it is its own thing, and carries the ordinary black rule beneath it; 02-CALYPSO keeps the corner it always had. The plate is a NAME as well — `01-GATHER`, numbered the way `02-CALYPSO` is, since prefix numbering is that column's own grammar — so the cohort's count is not glued to it: holding something lights the block, holding nothing dims it, and how many is what the face says on its own state line. The lesson is the one this document keeps relearning in different clothes: a new part joins an existing grammar by reading what the neighbours DO, not by matching the shape of their class names. |
| 2026-09-18 (the cohort rides the header) | Settled with the operator over a long grill, and the operator's argument won the shape: "the header shows the same stuff no matter where the body panel is focused. If GATHER were there, the main body panel is invariant." A cohort spans PACS series and will span directories, so it never belonged to a pane — and a companion pane rearranges the workspace on every single gather, which is a cost paid forever. It is now the header's THIRD FACE, on the mechanism the other two already use: a button selects it, the selected button pressed again sends the band away, and `data-header` carries the state. The face is a proper field — rule, elbow, spine — because a cohort is a listing and the moment it stops looking like one the rules fork: its rows' verbs ride the row zone, PULL and REMOVE ride the frame. Three decisions taken deliberately. The band is ONE height for every face, since a face that sizes to its content moves the workspace beneath it, which is the very thing this design exists to prevent. The boundary the band shares with the body is a drag strip, the console's rule read from the other side — a floor so it stays grabbable, a ceiling so it cannot crush the workspace, and the height remembered. And gathering REVEALS the band the first time, then respects a dismissal: after the operator sends it away, later gathers move only the count on the button. The cohort is written to `~/gather/current.json` as the session's own working state, so a refresh keeps it and a second surface attached to the same session sees the same cohort; SAVE still writes the named manifest, which is the artefact meant to be kept. Still to come: the pane escape for a cohort too big for a band, member KINDS so a directory can be gathered, and PROCESS made honest on a mixed set — which waits on the kernel question of rooting one feed on several places. |
| 2026-09-18 (a cohort is a reflection, so it moves) | The operator, after gathering a study and pulling it: "the GATHER listing did NOT show any progress — I assume the actual series rows in the PACS table were where the action was. A user would expect the GATHER to likewise track any activity on the series: the gather is just a link/reflection of the same truth." Exactly so, and it was a copy rather than a reflection: the PACS pane kept ONE badge element per series and repainted that, so the cohort's row was painted once at render and never again. The badge elements are now a registry keyed by the series UID holding every element standing for it, in whatever pane; a progress tick repaints them all and drops the ones that have left the document. Proven live: a cohort's own row walked NOT RETRIEVED → RETRIEVING 0/1 → ✓ PULLED during a pull started from the cohort's frame. Same report: gathering a STUDY left the operator unable to tell which study they had taken, since a column of series descriptions says what each one IS and nothing about where it came from — the cohort carries STUDY now, beside MRN. |
| 2026-09-18 (a fold is a way of looking, not a way of choosing) | The operator: "when I land a query, in its frame I only have FILTER. I should have a PULL there — to pull whatever is in the table. I do see PULL if I filter, but I don't think the UI has the right to decide no PULL for the full original table." It does not, and it was not deciding: it was counting. A field verb read "what is shown" off the PAINTED rows, and a fresh answer has every study folded, so the count was zero and the verb withdrew itself on exactly the table an operator most wants to act on. What is shown means the filtered set while a filter is on — the listing opens what the filter kept, so the painted rows ARE it — and the WHOLE answer when none is, because folding is a way of looking and reading it as a choice makes the surface decide something the operator never said. The blocks say which they mean: `PULL 245` on a fresh answer, `PULL 16 SHOWN` under a filter. Same round: the cohort's frame gains REMOVE, which takes every member out — a row's REMOVE takes one, and emptying a cohort one row at a time is not a gesture anyone wants. DISMISS stays what it was, which is forgetting the cohort entirely rather than emptying it. |
| 2026-09-18 (a cohort shows what a series shows, and a frame pulls) | The operator: "the GATHER pane shows only a subset of the normal series elements — it must show exactly what the normal series shows. The GATHER is an organized subset, not a new thing. And the frame on GATHER, and the PACS query table frame, should offer verbs, specifically PULL, since that's really what someone wants to do." Both done, and the first was two vocabularies for one row: the cohort described a series in columns of its own — description, modality, MRN, files, a word for state — while the answer it came from used SERIES, STATE as a badge, MODALITY, FILES. One declaration now serves both, exported from the PACS pane and read by the cohort, so a gathered row carries the same state badge (`✓ 26 IN CUBE`, or NOT RETRIEVED) as the row it was taken from; the gathered entry carries the series itself, and MRN rides along because a cohort may span patients where a study's series never do. And the frames carry the verb that acts on the whole listing: PULL n SHOWN on the PACS frame, which is the filtered set when a filter is on, and PULL n on the cohort's, which fetches the members CUBE does not hold. One command over many operands, because twenty pulls is twenty lines to audit and the kernel already takes a list. |
| 2026-09-18 (`..` is the way out, not a row about a place) | The operator: "hitting `..` should be navigation only." Right, and the reading was wrong in a way that matters: verbs on `..` were computed for the PLACE ON STAGE while standing on a row that names the PARENT, so MKDIR reached from there said "make a directory in this listing" from a row meaning "up". The lead row is offered nothing now, and the façade's own rule does the rest — a row with nothing to hide keeps its single click, which is exactly what going up should cost. The field's verbs were never `..`'s to lend: MKDIR, UPLOAD and REFRESH ride the frame, which is what answers to the field. This settles the shape the operator stated for the whole surface: a row is selected and its verbs appear in the frame's row zone; the listing's own verbs are reached by opening the frame, and everything in the frame acts on the whole listing. |
| 2026-09-18 (a question stands where the press was) | The operator: "I can MKDIR correctly with an action pane appearing in the file listing context, but if I try and delete a dir the yes/no still surfaces only in the terminal — fix please." Right, and the line drawn earlier was too clean. A question the SESSION asks does belong in the console — except when a pressed verb provoked it. `rm -i` asks before it removes, and the operator is looking at the row they just acted on, not at the console. A verb that lowers to a command now says which pane pressed it, and a question arriving while that command runs stands on that pane: YES / NO / ABANDON, console shut, transcript still keeping the exchange. The claim is cleared the moment it is used, so an unrelated question later cannot inherit a stale pane. Same round, same operator: the CSV listing had no frame to open. Two rules were reaching past their own pane — the content view hid `.mode-frame` by DESCENDANT, so a view that brings its own field had that hidden too, and the field variables (`--spine-w`, `--frame-top`) were declared on a fixed list of containers the new field was not on. A field is laid out the way the other fields are, or it is laid out wrong. |
| 2026-09-18 (a CSV is a listing) | The operator: "how are CSV files viewed — as a table or raw text?" Raw text, verified by opening one: a `pre` of quoted lines. Which is odd, since the file is a table produced by a table, and this surface renders every table through one façade. A delimited file now opens as a listing: its header record becomes the caps, its records the rows, and it gets the frame, the sort and the filter every other listing has. RAW stands beside CLOSE for when the quoting is the thing being read. The file is READ rather than split — a field may carry the delimiter, a newline or a doubled quote, and splitting on commas turns one such row into several wrong ones — and a first record that is not a header (a blank, a repeat, or a bare number among its cells) is kept as DATA with the columns numbered, because inventing names out of values would hide a row. The view is bounded at five thousand records and the bar says so when it bites. Two layout faults were caught by looking at the pixels rather than the DOM: hand-rolled absolute insets put the rows above their own view, and the caps row needed the same seating every other field gives it. A field is laid out the way the other fields are, or it is laid out wrong. |
| 2026-09-18 (a control nobody can see is not there; a file nobody can see is not written) | The operator, running their own flow against a live PACS: "I search 1279049, I filter for sag, the table adjusts, and I see NO MECHANISM to GATHER this. I hit EXPORT CSV, did a MKDIR, got WROTE /home/…/csv/pacs-….csv — and nada. Nothing." Three faults, and the first two were mine for verifying properties instead of pixels. GATHER SHOWN was put on the results MODE FRAME, which is the closed spine at rest: measured live it was 22px wide and `visibility: hidden` — the identical trap this document already records for EXPORT CSV, repeated. It now stands beside EXPORT CSV on the command row, where the whole answer's verbs live. Second, GATHER was offered only on a series already home, so on a fresh answer — where nothing is home — the gesture did not exist at all. A cohort is a set of TARGETS and the feed it roots is made by a pull over its members, so a series is gatherable once it can be NAMED: a PACS path, or the fact it has landed. Query, filter, take what matched, fetch it as a set. Third, the export was not broken at all: the file was written, and `cat` returned it, while `ls` and every browser showed the folder without it — the CSV writer wrote straight to CUBE and never invalidated the folder's cached listing, which every other fs verb does. So the operator opened the folder their table had just landed in and saw everything except their table. The WROTE readout also acts now: pressing it opens the folder, since a file written where nobody can see it is one they have to go hunting for. |
| 2026-09-18 (a table is gathered) | The operator: "GATHER currently only works per SERIES — a WHOLE study should be gatherable, including a whole TABLE. And a table GATHER must be scoped to whatever is currently shown, i.e. filtered." Both now exist, and the cohort did not change: it holds series, and what was missing were ways in. A STUDY row carries GATHER when any of its series is home, takes those, and says in the transcript how many it took and how many are not in CUBE — pulling is the other verb, and pulling gathers what it pulls. The TABLE's GATHER rides the FRAME, because it acts on the field and the frame is what answers to the field; that also settles the scoping by construction rather than by a rule of its own, since the listing paints what the filter kept. It reads `GATHER n SHOWN` and stands down at zero, a readout that acts saying what it will do. The rule inside it: it takes the SERIES the filter left standing, never the studies they hang under — a study survives because a child matched, so sweeping its siblings in would gather exactly what the filter excluded. And a filter now OPENS a row it kept for a child's sake: a filter that hides its own matches behind a fold has not answered, and a verb reading the field would not see them either. Not offered on a patient: one press that takes hundreds of series is a press nobody can undo comfortably; filter, then read the count on the block. |
| 2026-09-18 (a question occupies the prompt, not the session) | The operator: "EXPORT CSV is dysfunctional. The MKDIR in the export filename line seems a stray and does nothing other than move UP one dir. And MKDIR in the frame just doesn't work — seems to suggest a clogged input queue." All three were one fault and one design error. The clog: the console refused to dispatch any line while it was busy, and the command that asks a question stays busy until it is answered — so every verb that lowers to a visible line queued behind an answer that had not been given, including the errand's own MKDIR, whose question was the very thing blocking it. Silent instrument traffic never used that path, so listings kept refreshing while verbs read as dead. A question occupies the PROMPT, not the session: a verb the operator pressed now goes around it, echoed as always. The design error: the errand carried a MKDIR that made the folder the path lives IN, so a field holding a directory made its parent, which already existed, and the browser appeared to walk up a level for nothing. The operator's ruling: "the user simply types the full path; the export mkdirs silently on the holding dir and if that fails reports back. No mess, no fuss, power terminal compliant." That is what the kernel already does — it makes the holding folder, says `made <dir>` and refuses by name when it cannot — so the bar's MKDIR is gone and the typed path is the whole answer. A browser's own frame keeps MKDIR, for making a place before choosing it, and it works now while the question stands. |
| 2026-09-17 (a projection offers what it can) | The operator, looking at a node's dive: "this is all in the /proc VFS so I'm guessing many verbs are irrelevant (like MKDIR)." Right. A projection is the kernel's rendering of something that is not a folder — `/proc`'s jobs, `/net`'s servers — and nothing writes it: there is nothing behind it to make a directory in, move a file into, copy within or remove from, and CUBE grants a feed on a feed's own path rather than on a view of it. Those verbs are withheld there now, on the rows and on the frame, because a control that cannot act is a promise the frame does not get to make. What a projection is FOR stays: a file offers DOWNLOAD, a series folder IMAGE, and a node's own data offers PROCESS — which is the whole point of standing in the graph. A row left with nothing keeps its single click, so `..` still walks and a child job still hops, which is the same rule that governs every listing. The feed a path names is now read from either address, the stored one or the projected one, since a node under `/proc` was being offered a NEW feed rather than the one it is already in. |
| 2026-09-17 (a frame asks the document) | The operator, on the node dive: "I select a file and I see the selection highlight but the frame doesn't open. Again, this reinforces my belief of multiple sources of truth here." The belief was right and the phrase is the diagnosis. A listing resolved the pane its frame belongs to ONCE, at construction, and kept it. The node overlay stamps its body, builds the browser on it, and only then appends the whole thing to the scene — so that listing's pane resolved to null and stayed null. The row indicated, the zone filled with its verbs, and the attribute that opens the frame was never written, because the listing was answering "where do I live" from a snapshot while the document held the truth. The façade asks the document each time now, and wires its frame watcher on the first paint that finds a pane rather than at construction. A unit test builds a listing detached, attaches it, and requires the frame to open. Related: the stale answer also cost the MKDIR smoke scenario, which went on typing its answer into the console after the question had moved to the pane — it read the ask in the transcript, answered nowhere, and still passed its ask check. |
| 2026-09-17 (a node carries its verb) | The operator: "when you click on a DAG node you get the overlay — there should be a pill also as part of the overlay that says PROCESS and kicks off the process interaction flow." Yes, and it belongs there rather than on the mode frame: the overlay already states what THIS node is, the operator is looking straight at it when they pick a node, and the one thing to do about a node acts on that same node. That is `a-control-lives-where-it-acts` read plainly; the frame answers to what the FIELD holds, and a graph's field does not change because one node was indicated. The verb keeps the name it has everywhere else — a directory, a node's data, a PACS series, a cohort — because one gesture with two names costs the operator a concept; what it will do is said by the catalogue it opens, whose binding reads `INPUT … → feed N · node M`. A collapsed ×N group withholds the verb and says why: an aggregate has no single instance for a run to append to, and choosing a representative silently would be the surface inventing an answer. |
| 2026-09-17 (a node's listing is a listing) | The operator: "in the DAG file listings inside a node, clicking an entry seems to navigate it, while everywhere else clicking selects and shows the frame's verbs. Why is that different? It should not be." It should not, and the cause was an omission rather than a decision. A browser declares its row verbs into the façade; the browser that overlays a node after a fly-in declared none, and the façade's own rule is that a row with nothing to hide keeps its single click — so that one listing kept the bargain every other listing had left behind, and its frame never opened. It now declares the same verbs, computed by the same roster, so a click indicates, a double click goes, and a node's own `data` is offered PROCESS inside the graph exactly as it is outside it. Law `a-row-is-indicated-before-it-is-acted-on`, which the overlay was quietly exempt from. Still missing, and named here so it is not mistaken for this: the DAG node ITSELF offers no verbs, so PROCESS on a node must still be reached through a browser. |
| 2026-09-17 (a stop says its name) | The operator: "a running node has the subway stops that need to be updated live. I feel that never happened. Also the overlay has the stops tiny." Both true. The lifecycle strip under a node's facts was six bare dots with the stage name only in a tooltip, which is a readout only a hand that already knows the lifecycle can read — and the one moment the strip earns its place is a node in flight, which is exactly when the stage has to be legible without hunting. Every stop now carries its name, the stop the node stands at is drawn larger as well as lit, and the words ahead of the line stay dim because what has not happened yet is not news. And the overlay is repainted whenever a fresh model arrives — the pane is already watching the feed while anything on it moves, so the facts on screen were the one part of the pane that did not move with it. A strip that says SCHEDULED while the job has started is a readout that has stopped being true, which is worse than no readout at all. Nothing here animates: a node moves when the feed says it moved. |
| 2026-09-17 (a question stands where it was asked) | The operator: "my console is closed. I hit SAVE and the confirmation surfaces in a different pane (the console) that also is closed. Transient asks MUST be where they are asked." Right, and the cost was not cosmetic: PROCESS on a cohort asked for a name into a drawer of zero height, so the press read as dead, the surface sat waiting on a question nobody could see, and the next press was refused because one was already open. A question nobody can see is a question nobody can answer. So a question the SURFACE puts — a name for this cohort, a title for this run, a name for this directory — now opens on the pane that asked, as a bar of its own across its head: the question stated, the value editable and holding the keyboard, one verb that commits reading what it will do (NAME IT, RUN, MAKE IT), ABANDON beside it, and a yes/no as two capsules instead of a field. It is exposed rather than merely present, the way the drawer glides, because it arrives under the hand that provoked it. The console still records the exchange through the note it already had, so the scrollback stays the whole story of the session; what moved is where the operator answers, not what the session keeps. A question the SESSION puts still belongs in the console, since that is where the session speaks — but asking one exposes the lid, for the same reason. Same round: the GATHER pane indicates its cohort row on arrival, since a listing of one row has nothing to choose between and the operator had reasonably read its fold capsule as the only control it had. |
| 2026-09-17 (a surface says how to reach it) | The operator: "a user, in argus, goes 'oh I want to attach a terminal here' — they need the URL, port and token." They do, and the surface already holds all three: this page reached the daemon by an address carrying the attach token, so `attach` in the console reads its own location and says how to reach the same session from a terminal (`chell --remote`, or `chell --remote --attach "<url>"` from elsewhere) and from another browser (the address itself). It asks the session nothing, because nothing needs asking — a surface that cannot say how it got here would be a surface that does not know. The token is a bearer credential with no expiry, so it is masked by default and revealed only by `attach --reveal`, the same discipline the console's own secret asks keep; and when the address is loopback the readout says so, since a second machine cannot reach 127.0.0.1 whatever the token says. The verb is the surface's, not the kernel's: what it answers is the browser's own address, which no other surface has and the session cannot know. |
| 2026-09-16 (HERE did nothing, so it is gone) | The operator: "what does HERE now do? Near as I can tell, nothing — a button that makes no sense." Right: its press re-entered the place, a re-listing of where you already were, and a capsule that reads as a verb and does nothing breaks the one rule capsules exist for. The `.` row it sat on was slice 2's answer to a directory that could only be entered; with the control column that directory is selected from its parent (MOVE / COPY / DELETE / SHARE FEED n there), NEW DIR was already MKDIR on the frame, and REFRESH acts on the field, which is what the frame's field zone is for. So the `.` row and HERE are gone, REFRESH stands on the frame beside MKDIR and UPLOAD, and `..` keeps UP, which does what it says. |
| 2026-09-16 (PROCESS: the catalogue is a listing, bound to a place) | The main gap in the surface, grilled with the operator to thirteen settled answers (plan: process-workflow): building compute. The console path was complete — `cd` into a place, then the executable's name with its flags — and the surface had no gesture for it. Now it has one, shaped like every other choice here. PROCESS is a verb on a PLACE: a directory selected in its parent, a node's own `data/` inside a feed (never a directory beneath it, since the kernel runs on the whole node whatever is named), a DAG node, a PACS series or a wholly-home study. It opens `/bin` as a CATALOGUE — the files pane wearing catalogue traits (NAME / TYPE / VERSION) — right of the pane it was pressed in, joined to its group, with a run strip at the head of the listing that reads what will be processed and where it lands (`INPUT … → new feed`, or `→ feed N · node M`) and holds the LINE the run will run, editable and authoritative as the PACS command line is. A plugin row's body selects it, RUN in the frame; RUN composes the line when the strip is empty — the `cd` the kernel takes its input by, then the executable, and for a new feed a title asked once with the place's name to hand — echoes it as if typed, runs it, and the kernel's answer, now a `run.scheduled` model rather than lines to parse, lights a FEED capsule on the strip that opens the run's graph beside the catalogue, joined. A refusal reads beside the row's verbs. Nothing moves on its own after a run: the catalogue stays bound for the next one. Rejected on the way, by the operator: a parameter table folded under the row (a second road to the graph, which already is the form), and any dropdown. Not here: editing parameters in the node dive (next), desktops carrying the binding and the line, RECENT, GATHER as a listing, cohort PROCESS. Law `process-acts-on-a-place` (smoke). |
| 2026-09-16 (the control is the kind) | The operator: "the OPEN and UP LCARS on file listings are superfluous? One click selects a dir (fine) and a double click enters (also fine). Same for UP which could/should be a dir fontawesome with `..`." Agreed. OPEN said the same word on every row that opens, which is every row, so it said nothing; and the browser already carried the console's glyph beside it, saying what the row is. The two cells fold into one: the control is the kind's glyph, capped in the kind's console colour when the row opens, bare when it opens nothing, and `..` is a directory wearing the folder with its name shown. PACS keeps OPEN / CLOSE, since there the fold's state is the information. Double-click still enters. |
| 2026-09-16 (the graph is the form) | Settled with the operator: a plugin's parameters are not a fold-level table beside the catalogue (a second road to the same place) but the node's own dive, now editable. Opening a plugin from a bound catalogue shows its one-node graph, and a level of one opens itself: the camera flies in and the parameters read as VALUE cells, each writing its flag into the run strip's line as typed — a boolean bare, a value quoted only when it needs it, an emptied cell removing the flag. The line stays authoritative: a hand edit stands, and a cell reads back what the line says. RUN rides the graph's frame as well as the row zone. A pipeline's node dives with the header `title · @id`, its authored arguments editing `--<node>.<param>` with the node its title when shell-safe and unique, else `@<pipingId>`, as the kernel resolves it; and since the kernel runs a pipeline only on a node, RUN off one is refused by name. A pipeline run now answers `run.scheduled` like a plugin's. |
| 2026-09-16 (a desktop carries the line) | The PROCESS workflow's third slice. A catalogue left for another domain returns from PANES as it was: bound to the same input (and feed or node), its run strip's line verbatim, RUN ready — the listing, not the dive, since what to run is a choice again — and the graph of a run opened beside it comes back beside it, in the same split. The desktop is a replay of actions as every desktop is: `catalogue` carries the binding and the line, `graph` the feed. And the catalogue leads with RECENT: the executables this operator ran lately, newest first, from the kernel's own instance listing (`plugininstance list`, now a model as well as a table), the feed root's copy left out — the last thing run is the first thing offered, and it is the same row /bin holds lower down. |
| 2026-09-17 (GATHER is a listing) | The PROCESS workflow's fourth slice, the operator's addendum: "GATHER becomes a listing pane". The tray under the PACS results — checkboxes and a strip of capsules — was the one place on the surface where a set was not a listing and a control did not live where it acted. Gathering the first series now opens a GATHER pane below the results, joined to the workspace's group, declared into the façade like every other pane: one block, the cohort row over its series. Membership IS the row, so REMOVE is a series' first verb, beside IMAGE and PROCESS for a series CUBE holds; the cohort row carries SAVE / EXPORT CSV / CREATE FEED / DISMISS, the verbs the tray's head used to, now on the row they act on. The cohort is named at its first SAVE (asked, a dated name to hand). DISMISS forgets it on the surface only — a saved manifest is a file in ~/gather. The PACS row's GATHER lights from the cohort, and the cohort travels with the desktop (`gather` carries the series and the name). A cohort's own PROCESS waits for the next slice. |
| 2026-09-17 (a cohort is processed) | The PROCESS workflow's fifth and last slice. PROCESS on the cohort row: the cohort's feed first — made, if there is none, by the one line the operator could have typed (`pull --new-feed "<name>" <paths>`, echoed and run, its name asked once) — then a catalogue bound to the feed's root, so a run appends to the cohort as it appends to any node. The kernel now answers a pull that made a feed with a `feed.created` model (the feed, its pl-dircopy root, the root's data folder), so the surface binds to the place rather than reading it out of the text. The cohort row lights `FEED N`, the lit capsule the catalogue's strip wears, opening the feed's graph beside the cohort; CREATE FEED is spent once the cohort has one; the feed travels with the desktop. The whole gesture, PACS query to running compute, is now listings and frames end to end. |
| 2026-09-17 (the roster settles itself) | The operator, on a run that had finished long before: "the DAG state never stabilized — it seems to show the feed running all the time." It did. The roster asked the element it draws into whether it was `block`; the roster is laid out as a flex column, so the answer was no, forever. Three behaviours rode that one word: the roster never re-asked (a finished run read RUNNING until the pane was rebuilt), a cwd inside a feed painted that feed's graph over the roster it was meant to leave alone, and an arriving feed that was not listed never prompted a look. A pane's own state is the pane's to hold, not something to read back out of a style property. The roster now keeps one field, and the beat follows the work: while any listed feed is live it re-asks every ten seconds, and falls back to a minute once every row is terminal — the roster is cache-resident, so watching a run costs a read and no visit. |
| 2026-09-17 (a result opens where it lands) | The operator, having run dcm2niix from the surface: from the console they could image the volume it made; inside the node's own dive the same file answered READ REFUSED. Two faults, one gesture. The dive's file handler knew only raster images and read everything else as text, while the browser beside it had long routed a volume or a DICOM slice to an image pane — one rule, two callers, and only one of them had it; it is now asked once. Underneath, the kernel could not serve the bytes at the address the graph itself gives for them: a node's `data` is a projection, `/proc` resolves it to the CFS file behind it, and the proc provider handed that file back to the dispatcher whose default provider is the HOST filesystem, which refused it. A projection now reads its target through the one door that knows both worlds, and the daemon's own file route resolves the path it is given rather than assuming CFS. The same bytes, one name or the other, and no longer one of them refused. |
| 2026-09-17 (a pane title keeps its line) | The operator, on a screenshot: "the title for the image pane is messed up." It was. A pane header is one line of a fixed height, and a long name — a series description, a converter's filename — wrapped inside it: the letters were cut top and bottom, and the mode and state readouts were pushed across two rows. In a header the NAME is what gives way: it ellipsises, with the whole of it on the element's own tooltip, and the readouts beside it keep their width. The rule sits on the header bar itself, so every pane kind inherits it, and the smoke suite now measures every visible header rather than trusting the eye. |
| 2026-09-17 (the console's zoom owns the stage) | The operator, on a zoomed console: "the top border is off screen, and it looks like the whole viewport has shifted up, showing stuff at the bottom outside the console." The drawer's zoomed height was a constant measured from the viewport — a whole screen less a guessed gap — while the drawer still began below the header's own. It therefore ran past the foot of the screen: the workspace showed under it as a band, and the page grew taller than the viewport, where an overflow already frozen for the zoom could leave the console's own top frame above the fold. A zoom is not a bigger box, it is the stage: the workspace leaves (as a pane zoom's siblings do) and the console takes what is LEFT of the column rather than a figure computed from the screen, which cannot overshoot at any window size or type scale. |
| 2026-09-17 (an elbow needs something to turn into) | The operator, on a zoomed console: "that top left weird join artifact." The frame's elbow is a JOIN, two halves of a square sat where a bar turns into the gutter, and it is what makes a bar read as turning rather than stopping dead. A zoom sends the gutter off stage, and the operator's own dismissal does the same, so the turn had nothing left to turn into: what remained was a dark hook hanging over the pane that now begins in that corner. The fillet goes with the member it joins to. Worth recording that the fillet is a pair of pseudo-elements, which is why it was invisible to every probe that reads elements — the same blind spot that lost seven of them in the clean-room rebuild. |
| 2026-09-17 (the contrast audit is a gate) | The operator, of a mocked landing page: "the UI contrast looks bad — isn't there some specific measurement here? I feel like yes and we did it." There is: WCAG 2.1 AA, 4.5:1 for normal type and 3:1 for large type and interface graphics, recorded in docs/WCAG-AA.adoc, which found and fixed three defects and then sat there as prose. A document cannot stop the next pairing from regressing, and the day the measurement became executable it found three more: the STALE readout at 1.38:1 in every scheme, the SIZE cap at 4.42:1 under two, and PHAROS still carrying the navy MEDICAL had been lightened away from, failing at the very ratio the audit records as fixed. The gate walks the frame's type on the live surface in all five schemes and composites every translucent layer between the type and its first opaque ground, because a chip painted black at .72 over gold IS the type's ground and skipping it measures a pairing that is not on screen. |
| 2026-09-17 (a session begins at the dashboard) | The operator, thinking about a new user meeting the surface: "lots of chrome — a header pane, a body split into a half-opened console and a file listing, chrome on the left. Overwhelming?" The count was the problem: a gutter, a header of instruments, a console and a listing all said START HERE at once, so none of them did, and a first screen has three questions to answer — where am I, what can I do, what happens next. The launcher answers them as blocks, which is what this frame is made of: one per domain, carrying what that domain holds right now and ending in the verb that opens it. Its rows are DOORS, not decoration — a feed opens its graph, a folder opens the browser there, and a PACS with nothing asked yet teaches the line it would take rather than apologising for being empty. The block with the most to say takes the wide seat. It is the EMPTY state, not a menu in front of the app: content opening takes the stage back to home, the word `launcher` returns, and a capsule on the screen itself says whether a session starts here. The gutter carries the way in, and carries it FIRST: SOURCES-04 had been an inert block with no domain behind it since the gutter was drawn, and the dashboard takes the top seat as DASHBOARD-01, the others each moving down one. The gutter reads top down, and what a session begins at belongs at the top of it; the `panel-N` classes are seats in the frame's hue cycle rather than a domain's own colour, so a domain moving up takes the seat's colour rather than carrying its own. The block, the pane and the word all say DASHBOARD. It re-reads what each domain holds on entry AND on its own beat while it is on stage, asking nothing when it is not: a dashboard read once is a dashboard that lies by the time it is read, which is the defect the roster was carrying this same morning. The rule that keeps it from becoming the wall it replaced, stated while it is cheap: A BLOCK IS A DOMAIN THAT EXISTS AND HAS SOMEWHERE TO LAND. A dashboard is the easiest place in a product to add something, so over time everyone adds theirs and the sum is undirected chrome arrived at by generosity instead of by history. FILES, ANALYSES, PACS and PANES qualify; SOURCES is configuration rather than a place work happens, and STATS has no pane to open — if it ever gets one, the block follows. Anything that is a figure rather than a place belongs on the header's instrument strip, which exists for exactly that. Rejected: opening zoomed on RUNS, which starts inside a mode whose exit a newcomer has not learned, hides the identity readout that answers the first question, and shows a new account a full screen of nothing. |
| 2026-09-16 (a feed on stage is a desktop) | The operator: "I had entered a selected RUN and chosen a visualization, left to another tab, and wanted to go back — nothing in PANES." Then, testing: "PANES only saves when a primary has more than one pane open." Both true, and the morning's row overstated: a runs domain's action carried nothing but the domain, so a graph on stage was not content, a lone RUNS-02 with a feed open never carded, and the card a second pane earned came back to the ROSTER, not the graph. The runs domain's action now carries the feed on stage and the view the operator chose — layout, projection, scale, hue, census, only where they differ from the defaults, as the lines that set them — so a graph counts as content, a lone RUNS-02 with a feed open cards under the feed's own title, and restoring brings the graph back as the roster would, then replays the view once the graph has landed. A bare roster is still not carded: one gutter press away. |
| 2026-09-16 (the roster joins, and the zone is the façade's to mint) | The operator, watching three panes get the same treatment one at a time: "listings are multiple sources of truth." The engine was already one — sort, filter, caps, grid, indication, zone, selection, control, dimming, block, all in the façade, and the lint forbids a pane building its own — but two things had been declared per pane that were not the pane's to declare: whether its verbs ride the frame (`rowZone`, made opt-in so slices would not break panes not yet converted), and a zone span in each pane's template for the façade to fill. Both gone. The façade now mints the row zone itself into the mode frame that frames the mount's field, above the frame's fill, so any listing whose field has a frame routes its verbs there with nothing declared and no template carrying a span; a listing with no frame keeps the in-row track. What remains per pane is what IS the pane's: its traits, its verbs, which trait is its control. RUNS-02 is the third pane by that path, and the cheapest: a control column whose OPEN enters the feed, its body selecting the feed and putting SHARE / DELETE and the grant readout in the roster's own frame; the in-row track and its 19em constant are gone. |
| 2026-09-16 (the browser navigates and selects the way PACS does) | The operator: "say I have a dir ~/data/a and I want to delete a. In the current nav-only browser I can't select a — I can only enter a and once entered choose HERE. Very awkward." It was, and it was the same conflation PACS had just been cured of, solved the other way round: PACS had one click doing both, the browser had one click doing only the navigation, and a directory that can only be entered can never be acted on. The fold control generalises into the row's CONTROL, one mechanism for both panes — the trait whose cell activates the row (folds it, enters it, opens it) and nothing else, while the body indicates and nothing else. The browser gains a control column, the PACS fold column's twin: OPEN on everything that opens — a directory enters, a file shows, a catalogue entry graphs — UP and HERE on the lead rows, nothing on a row that opens nothing; the glyph column goes back to icon only. With entering off the row's click, a directory is acted on like a file: MOVE / COPY / DELETE, SHARE FEED n inside a feed, IMAGE added on a series folder — the deferral slice 1 made "for the single-click path" is undone by the gesture that removed the reason. Double-click stays as a shortcut and nothing depends on it. |
| 2026-09-16 (a retrieve in flight keeps its row) | The operator, pulling a series: a selected row "is clearly selected and no layout changes happen" — then the pull starts and "for no good reason" RETRIEVING drops beneath the bar, the bar runs out past every other bar, and the row grows. The badge's grid collapses to one track for a state with no bar (an error, an unconfirmed pull), and the running bar is its own element, which that rule did not list; so the one row being watched was the one that moved. It keeps its two tracks now: the bar ends where every bar ends and RETRIEVING 0/75 stands where NOT RETRIEVED and ✓ 75 IN CUBE stand. The running track's hardcoded orange takes the state token, and on the indicated block the running bar inverts to black like every other bar, which is the contrast the operator saw fail while the row was selected. Nothing about a row changes shape when its state changes; only what the cells say. |
| 2026-09-16 (a control in a row is a capsule, and the browser speaks the console's vocabulary) | Two questions from the operator on the fold control. Is a lone triangle canon? No: the language has no disclosure carets; every control is a block or a capsule that reads as what it will do next, which is already law (`control-reads-next-action`). So the fold cell is a capsule now, OPEN on a closed row and CLOSE on an open one, the words drawn by the stylesheet from the group's open state so they can never disagree with it, nothing at all on a patient with no studies; the column widens to hold the longer word. The browser's two lead rows take the same form: `..` wears UP, `.` wears HERE, each a capsule in the glyph column, so a control is never an 8px glyph beside a row that does something else. And should the browser share the console's icons and colours? Yes, and the argument is the stronger one: one surface, one vocabulary — a folder `ls` just printed yellow with the Nerd Font folder glyph should be the same yellow folder one pane over. The console's palette is a fixed ANSI table in the terminal renderer and its icons come from `packages/chili/config/colors.yml`; the console now publishes its palette as `--console-<name>` tokens at boot and the browser's rows read them (dir yellow bold, file white, link cyan, plugin green bold, pipeline and vfs magenta bold, a job the console's cyan), carry the console's Nerd Font glyphs in the console's face, and a unit test holds the browser's icon and colour tables equal to the console's file so a change to one is a change to both or a red test. The `▸ · → ⚙ ⛓ ◆ ▷` set that used to stand in is gone. Law `control-reads-next-action` (smoke). |
| 2026-09-16 (the frame takes PACS, the place has a row, and a verb lights where it now lives) | Slice 2 of the row zone. PACS declares its zone: no level reserves a track any more, a series' GATHER / IMAGE / DIR / PULL and a study's PULL STUDY are drawn in the frame when their row is indicated, and QUERY, which stood in the study grid's verbs column, takes the SERIES column the form leaves blank (widened to hold it). Three rules of the façade followed from routing a folding listing. A row that FOLDS stays on stage when activated, so a click on it can fold; and the operator's review settled that navigating and selecting are two intents that must not share a gesture ("if I hit the triangle I am not selecting a study — my goal is to see the series"): a folding level names its FOLD CONTROL, the trait whose cell folds the row and does nothing else — no indication, no verbs, the field stays lit — while a press anywhere else on the row indicates it and does not fold. The control is drawn as one, since a bare 8px glyph beside a row that does something else punishes a near miss with the wrong intent: a small block in the frame's hue, dim at rest and lit on hover, inset so a miss lands on black, in a column widened to clear the 24px target minimum. A study has no double-click. A listing holds ONE indication across its levels: a series indicated beneath an indicated study stands the study down quietly, so the zone is always one row's. And the indication belongs to the FIELD, as the selection already did: a fold, a sort, a re-listing of the same field rebuild the rows and the indication is restored onto the new row without the pane being told again; a row that left takes it with it; navigation clears it. That last rule is what lets a fold keep its row lit through the repaint it causes, and it is also how the #580 lit state moved into the frame: the indicated row's verbs are redrawn from their live predicates on every stage change (`indication_refresh`), so GATHER lights when the series joins the cohort and IMAGE when its viewer opens, in the zone, without the listing re-rendering — verified live, both capsules lit in turn. The browser gains the place's own row: `.` after `..`, outside the order, offered NEW DIR / REFRESH / DELETE (and SHARE FEED n inside a feed), which is where what may be done to the directory on stage now lives — a wide target for the place, where the spine is a thin one. A lead row is judged like any other: `..` is offered nothing and keeps one click; `.` is offered verbs and indicates. Verified live: `.` indicated puts its three verbs in the frame, REFRESH re-lists with the indication held, NEW DIR asks in the place and the directory lands, DELETE lowers to `rm -ri` and the kernel asks. Two things from the operator's review of it. A bar on the indicated block vanished into the block's hue, and the idle NOT RETRIEVED track was a navy wash at about 1.2:1 on black: on the block every bar is now black, and an idle track everywhere is an outline in the frame's hue. And PULL STUDY, at its fixed place on the bar, lined up with a stranger — the study it belonged to had scrolled to the foot. The operator's answer, taken: while one row is the block the rest of the field recedes to a quarter (45% first; the operator asked for a lot dimmer), so the block is the only bright thing in the field and the frame the only bright thing beside it, and the eye pairs them; hover restores a row, and SELECT (many rows marked) is exempt. The PACS rows' hover tint, a hardcoded orange, takes the frame's token on the way. Not here: the roster, and multi-select (slice 3). |
| 2026-09-16 (every layout is a desktop, not only a viewer's) | PANES-06 only ever held desktops that had a VIEWER in them: a wall of file browsers, or a runs layout, left nothing behind, because capture keyed the whole card on an anchor it took from the first image tile and bailed when there was none. A desktop is any arrangement with content beyond the bare domain now — a single primary pane is still not carded, since it is one gutter press away, but a second browser, a DAG layout, anything more is. A viewer desktop is still keyed by its series so returning updates the one card; a viewer-less one is keyed by the SET of content it holds (its browsers' folders, its panes) so the same arrangement updates its own card rather than minting a new one, and reads `FILES · 2 panes` or the folder it opened rather than a series it does not have. The mini-map figure was already domain-blind, so it draws these the same. Verified live on the isolated daemon: a two-browser files layout left behind a `FILES · 2 panes` card that restored both browsers. Law: every-layout-is-a-desktop.
| 2026-09-16 (a row's verbs live in the frame) | The operator's file-controls screenshot: 09-intrepid.png selected, DOWNLOAD / MOVE / COPY / DELETE drawn on the row at its right, and a column of dead space on every other row reserved for verbs nobody had asked for. That column was the action track, and it was the second verb system the façade carried: the browser's SELECT already put a selection's verbs on the frame, PACS put a row's on the row, and a third pane would have had to pick. Both now resolve into ONE place, the frame's ROW ZONE: a listing that declares one (`rowZone`) mints no track at all, its NAME expanse grows into the freed width, and indicating a row draws that row's verbs in the zone — one under another as the frame's other blocks stand, beneath the field zone (LIST, FILTER, HOME, BACK, MKDIR, UPLOAD, SELECT), which stays — while the row itself becomes a SEGMENT of the frame — the frame's block colour, the header bar's black type, run out to the spine — so the row and its verbs read as one shape; the operator's own call on review, over a first draft that tinted the row ("more like the frame color to show the strong relationship"). Verbs arriving open the frame; the last of them leaving retracts it, unless the operator opened it or has pressed something else on it since; and the frame retracting (Esc, the strip, a touch on the field) stands the row down, so an indicated row and an open frame are one state. A selection's verbs ride the same zone, and the selection bar is gone. The click rule follows from what a row is offered: a row with verbs splits click (indicate) from double-click (activate); a row offered none keeps its single click — so a directory, now offered none, is entered with one click as it was before #454, and what may be done to a directory as a whole (moved, copied, removed, its feed shared) waits for the selection, out of the single-click path. The one exception is a DICOM series folder, opened as an image far more often than entered: it keeps IMAGE and the split. Opt-in per listing, so the runs roster and PACS keep their in-row tracks untouched until their own slices route them here. Verified live on the served build: the file's four verbs stand in the zone, the frame slides in with them, no row holds an action cell, a field touch and Esc each retract and stand down, COPY lowers to `cp "…"` and asks where, SELECT keeps the frame and gathers `DELETE 2 / MOVE 2 / COPY 2` into the zone. Found on the way: a pane too short for the field zone plus a row's verbs ran the last of them off the pane's foot unseen, so the frame now scrolls. Not in this slice: PACS (fold-parent rows that fold AND select, PULL STUDY in the frame), the `.` row that gives the directory on stage its own verbs, the #580 lit state moving into the zone, and the roster with multi-select. Law `a-row-s-verbs-live-in-the-frame` (smoke).
| 2026-09-16 (a replay opens frames first, loads in parallel) | Restore replayed its action log one pane at a time and WAITED for each viewer's series to finish loading before opening the next — a whole desktop took as many seconds as its viewers' loads laid end to end, with fixed `sleep`s between. The frame of a pane, though, is on stage the instant it is spawned and split; only the pixels are slow. So replay is structure-first now: each open hands back its pane id synchronously (a new `onOpen` on `image_open`), the whole arrangement is built in one pass with the ids resolving every target, and the slow parts — a series' header read, a query — load in PARALLEL behind the frames. Nothing waits on another pane's pixels; the `wait_until` polling and the fixed `sleep`s are gone. Saved view state (layout, slice, window/level) lands on the viewer's OWN completion, never blocking the rest. The one thing that must finish first is the domain switch — its orphan sweep would dispose the panes opened after it — so it calls `domain_enter` directly (synchronous) rather than a deferred console line. Verified live on the isolated daemon: a viewer-plus-browser desktop's frames returned in ~90-250 ms (were seconds), geometry byte-identical, the series loading behind; smoke `image-panes` green. This is also the substrate the shared-session collaboration layer needs — a receiver replays one streamed action the same way, frame now, pixels behind. Law: a-replay-opens-frames-first.
| 2026-09-16 (a row's verbs light with what is on stage) | A series row's verbs now carry a persistent lit hue when their result is on THIS surface's stage: IMAGE lit when a viewer regarding the series is open, DIR when a browser on its folder is, GATHER when the series is in the cohort. PULL stays plain — its result is the series turning inCube, which the STATE badge already tells. The state is DERIVED, never a click-flag: the surface recomputes it from the live panes (a pane's kind and its regard address) on every stage change — a pane opened or closed, a regard written, a domain entered, a desktop restored — and toggles the class on the rendered rows in place, so the listing never re-renders or jogs. Because it is derived, it is honest across a close (the verb goes dark) and free on restore: the replayed intents recreate the panes, the bus reflects them, the verbs light — so scrolling the series list after a restore is a trace of what was visualised and DIRed. One engaged hue reads across the pills (the deep fill #579 gave a press, now also the persistent selected state; bright is available, deep is engaged, dimmed is off), clearing WCAG AA at ~7:1. A viewer anchors on the folder with its trailing slash stripped and a browser keeps it, so the key is normalised (`folderKey`) or a series would light one verb and not its twin. Scoped to the local stage — a teammate's viewer on another CALYPSO does not light this surface's row; the shared-session layer is a later, deliberate one. Law: a-verb-lights-with-its-result. Built after a grill (verbs, live vs snapshot, local vs cross-surface, one hue). Address matching holds by construction; the live series press needs an inCube series the jailed daemon cannot script, so the pill-lights-on-press step is operator-verified.
| 2026-09-16 (a pill press moves nothing) | Pressing a series-row verb jogged the whole row sideways and carried the capsule with it. Gathering a series grows the cohort tray below the listing, and the tray and the listing share the pane's height, so the listing lost height, its field gained or lost a scrollbar, and every row and verb shifted by the scrollbar's width — a press that reads as a layout the operator did not ask for. Three things settle it: the PACS workspace reserves the scrollbar's gutter whether or not it shows (`scrollbar-gutter: stable`), so a scrollbar coming and going no longer jogs the rows; the cohort tray scrolls within a bounded height instead of growing without limit and stealing the listing's; and a pressed capsule reads as a press IN PLACE — it darkens to a deep hue, changing only its fill, never a border or padding that would reflow the row. Verified live on the isolated daemon: a real pointer press on a capsule left its position unchanged and darkened its fill.
| 2026-09-16 (a dismissed header keeps the panes' frames) | With the header dismissed and the console closed, both panes lost their top load-carrying frames — the bars that carry their controls — off the top of the page. Header-away reclaimed the header's space by pulling the workspace up with a negative margin sized to the header's measured height; in the flex column that pull AMPLIFIED (a -368px margin dragged the workspace ~500px) and, with the console closed so the workspace already sat near the top, it overshot past the top edge and carried the pane frames with it. The header now LEAVES THE FLOW when dismissed — taken absolute and glided off the top by its own height — so the workspace flows up to fill the top exactly, deterministically, console open or closed and whatever the header's height. Same family as the zoomed-pane-loses-its-frame fix, one layer out. Verified live on the isolated daemon: header dismissed with the console closed, both pane frames sit at the top (were clipped to -26, now +158); the console-open case, the restore, and a zoom all still hold their frames.
| 2026-09-16 (the gutter dismisses from its own edge) | The header sends itself off the top on a second press of its face, leaving a pulsing strip to restore it — but the left body gutter always stayed, and a wall of panes had no way to take its width. The gutter now dismisses the same way: a press on the domain ALREADY shown (FILES while in files, PACS while in pacs) slides the gutter off stage left and hands its width to the workspace — a focus of the whole FIELD, distinct from a single-pane zoom that fills with one pane and hides the rest. This does not break the gutter law that a press renders its target deterministically: a press that NAVIGATES still lands you in that domain unchanged; only a press on the domain you are already in — where there is nothing to navigate to — carries the dismissal, exactly as the header's press-the-shown-face does. The state is its own (`data-gutter='away'`), orthogonal to the header and to zoom, so the three compose: header away, gutter away, or both for a full-bleed wall of panes, each restored from its own edge. A pulsing left strip (the twin of the header's) or Esc brings the gutter back; Esc peels one layer per press, a zoom first, then the gutter, then the header. Not persisted across a reload — a reload dresses the chrome again, as the header does. The slide reuses the reclaim a zoom already had (`.left-frame` off, `margin-right: -1 * --lfw`). Law: chrome-dismisses-from-its-own-edge. Verified live on the isolated daemon: a press on the active domain sent the gutter away and the workspace grew by the gutter's width, the strip and Esc restored it, and the Esc chain peeled gutter before header.
| 2026-09-15 (a pane's content cannot shove the layout) | With a viewer beside a PACS listing and a file browser opened between them, the operator saw the body shove right as the browser resolved a long CFS path in its narrow space — the viewer's frame ran off the right edge and the header's centre notch fell out of line. Three flex/grid boxes carried `min-height: 0` but not its horizontal twin `min-width: 0` — `.files-panel`, `#pacs-workspace`, and the sticky `.files-path` breadcrumb had none — so a wide child (a CFS path is a long, mostly unbreakable token, `/` being no break point) could set a min-content floor wider than the pane and push the whole row. Each now carries `min-width: 0`, and the path breadcrumb wraps within the pane (`overflow-wrap: anywhere`) rather than establishing a floor. A pane's content scrolls or wraps inside the pane; it never resizes the layout around it. Verified on the isolated daemon that a populated PACS listing, a viewer, and a browser on a 130-character path hold the layout at the viewport with no horizontal overflow.
| 2026-09-15 (a zoomed pane keeps its frame when the header is already away) | The operator dismissed the page header — the body expands to fill, as it should — then zoomed a pane, and the pane's top frame, the bar that carries its controls, rode off the top of the page. Zoom is the mode that reclaims the header's vertical space by sliding the content wrap up by the header's height; the header-away state slides the same wrap up by the same height. With both on, the wrap went up twice and took the pane's top with it. Zoom now OWNS the presentation: entering zoom sets the away state aside (restoring it on unzoom), so a zoom from away runs the exact geometry a zoom from a shown header does — the one that always worked. And because clearing the away state leaves the header mid-slide, unmeasurable, the slide distance is read from the header's last rested height (`headerRestHeight`) rather than a mid-transition measurement that came back short and left the pane hanging below the top. Verified live on the isolated daemon: header dismissed, a viewer zoomed, its frame bar and right-spine controls sit at the top exactly as they do with the header shown; unzoom returns to the away state.
| 2026-09-15 (a pane replays as it was born — split and all) | The operator stacked a linked filesystem BELOW a viewer, froze the desktop, and got back four panes in one horizontal row — nothing like the layout, and the card showed the wrong thing too. The cause was a shortcut: capture recorded every pane's split as the column-after default, never the real orientation and side, and the drawer's SPLIT-pill panes (a linked browser, a slaved viewer, a blank pane) were not recorded at all — so a below-split and a linked pane both vanished from the log. The model is now literal: a pane is nothing but its birth — the pane it split FROM, the split's orientation and side, and, for a pill pane, the binding it was made with — and a desktop replays by re-running each birth. Every open path records its real placement (`paneBirth`); the drawer pill and the replay share one spawn (`pillPane_spawn`), so a linked FS returns as a linked FS, stacked where it was; and a replayed open reads the captured split (`replayPlace`) instead of forcing a new column. The card figure is a true MINI-MAP now, each pane drawn at its measured position and size scaled into the figure, so a stack reads as a stack. No semantics enter into it — a pane is not restored by what it means but by how it was made. Verified live on the isolated daemon: a viewer with a linked FS stacked below it, sent to PANES, came back with byte-identical geometry (the FS below the viewer, not beside it) and a mini-map card showing the stack; smoke `image-panes` still green.
| 2026-09-15 (a desktop is a log of actions, and its card is the whole set) | Two refinements that finished restoration. First, ACCOUNTING: the operator caught that replay had no record of WHERE each pane was opened from — it reopened everything from the domain by convention, so a viewer or browser deliberately split off a non-obvious pane would return in the wrong place. A desktop is now a LOG OF ACTIONS. Each pane records its ORIGIN (the pane it split from) at the moment it opens; capture walks the stage in creation order and emits an action per pane — the domain is action 0, and every viewer/browser/tags carries `target`, the log index of the pane it was launched from, with its captured placement (`dir`/`side`; the ratio stays emergent at 0.5). Restore replays the log against a `produced` table: each action's `target` resolves to the exact pane that action split from, and the open is launched FROM it — so PACS | DIR | viewer returns in that order and a pane opened from somewhere non-obvious returns there too, not from the domain. This is the same shape as the shared-session idea, an action being a verb plus where it acts; the column-after default here is unchanged, and the BSP longer-axis default stays a separate follow-up. Second, the CARD FIGURE: a card showed one lone viewer thumbnail, so a PACS | DIR | viewer desktop read as just its viewer. The figure is now a STRIP of the whole set — each shown pane drawn as a tile in on-screen left-to-right order (sorted by each pane's measured position, NOT creation order, since a viewer opened before a browser can sit to its right — the first cut walked creation order and drew a PACS | DIR | viewer set out of order while restore, which reads the captured placement, put it back right) at a width proportional to the width it held on screen, a raster where the pane has a canvas and a kind glyph where it does not (a listing, tags) — so the card reads as the arrangement it restores. Verified live on the isolated daemon: a viewer beside a files pane came back a two-tile strip in order; smoke `image-panes` still green (the group cards after a domain switch, restores on press, brings its domain back beside the viewer). |
| 2026-09-15 (PANES is a desktop switcher; a card is a whole arrangement) | The rework that made restoration land. The first cut carded individual series and restored a viewer beside a stray files browser, losing the PACS context the operator came from — because a snapshot recorded only the series, not the domain and tiles around it, and the gutter switched the preset before anything could capture the arrangement. Re-grilled: restoration IS multiple desktops. A PANES card is now a whole ARRANGEMENT — the domain, its content, and its tiles — captured as a REPLAY of console actions (`view pacs`, `pacs query …`, `image <path>`, `image layout mpr`, `image tags`), not a pixel snapshot. Restoring replays those lines, so the viewer's own open splits it beside PACS exactly as it did live, and the whole desktop returns. Capture happens at one `domain_enter` chokepoint every gutter domain routes through, before the preset changes, so no domain is missed and the context a switch would lose becomes a card. Cards are keyed by the on-stage viewer's series (recognisable, deduped — returning updates the one card); a bare domain with no viewer is reachable from its gutter button and is not carded. This unifies with the existing `desktop` feature — a card is a desktop, auto-captured on leave, and `desktop save <name>` still pins one. The narrow-PACS clip (a series row wider than a half-width split pane) is resolved where it lives, in rendering: the workspace scrolls in both axes rather than clip its last verb, and replay inherits that for free. Geometry is whatever replay produces — default split ratios, no captured divider drags. Verified live: a viewer left to RUNS came back a card and restored WITH its domain beside it, its script the action sequence that built it. |
| 2026-09-15 (the DIR pill fits) | The DIR verb made a landed series' row carry three capsules — GATHER, IMAGE, DIR — where its action track was declared for two (12em), so DIR clipped at the pane's right edge. The track grows to 16em, which the three capsules clear with room; it stays a fixed track, so every series row reserves the same width and the caps row stays aligned. Measured in the pane's own font context: the three capsules total ~249px against the 16em (~352px) track. |
| 2026-09-15 (PANES is a primary, so it survives leaving) | A fix to PANES-06. The gutter's three original domains — files, dag, pacs — are primaries the orphan sweep spares, retained off-stage so a domain switch never disposes them. PANES was added as a domain but not added to that spare list, so navigating to RUNS or FILES disposed the PANES pane itself; pressing PANES-06 afterwards raised an empty pane with no card grid. The one flow that missed it in review went straight from a viewer to PANES, never through another domain first. PANES joins the primaries the sweep spares. Guarded now by a smoke scenario that moves away to RUNS before opening PANES and asserts the group is a card there and restores on press — the ordering the first check skipped. Found by reproducing the operator's exact flow against the live session. |
| 2026-09-15 (DIR: the same landed series as a browser) | Slice 4 of pane-group restoration, and the operator's side question answered. A PACS series row offered GATHER (stage an in-CUBE series into a `~/gather` cohort), IMAGE (view it), PULL (retrieve it from the PACS into CUBE). It gained DIR: the series' own CFS folder opened as a file browser, under the same gate as IMAGE (`inCube && folderKnown`), because DIR and IMAGE are two faces of a series that has landed — one drawn, one listed — while GATHER and PULL are how it lands and what it is gathered for. The browser opens JOINED to the series' group: when a viewer for the series is already on stage it lends its group, so the viewer and the browser are one restorable group (its PANES card badges VIEWER and FILES); otherwise the browser anchors the group on the folder itself. A browser already showing the folder is reused, not duplicated. Verb order on the row is GATHER, IMAGE, DIR, PULL. Units cover the offered set (DIR gated exactly as IMAGE) and the rendered order; the row press needs a landed PACS series to exercise, which a PACS-less daemon cannot stage, so the press path is verified by unit and by the shared spawn/root/group code the viewer's open already proved live. |
| 2026-09-15 (PANES-06: the groups you moved away from, to bring back) | Slice 3 of pane-group restoration, and its face. The inert COMPUTE-06 gutter slot is now PANES-06, a full-workspace domain like FILES/RUNS/PACS: it draws the dormant set (slice 2) as a grid of cards, one per group. A card carries the viewer's thumbnail (or a glyph when none captured), the group's label from its anchor (`T1 SAG · MR`), and a badge per member; pressing the card restores the WHOLE group onto the stage — the series or volume reopened at its saved layout, slice, window/level, colormap and ghost, its tags member reopened — while the small DISMISS is the one gesture that forgets it. Opening PANES sends the current group dormant, which is free and recoverable: it is itself a card in the grid. It is a domain, not a floating overlay, because LCARS has no floating chrome and the gutter already teaches "press a domain, the stage becomes it"; the overlay's one merit, not disturbing the current stage, is moot when leaving is recoverable. The verbs the operator reads follow tmux: the gutter domains are windows for the three primaries, PANES the window list, desktops the named saved sessions. Verified live: a viewer set to MPR at slice 5 with a colormap, sent to PANES, came back a card and restored at slice 5 with the colormap; DISMISS forgot it. Units cover the registry and the card grid (render, glyph fallback, empty note, restore-whole-group, dismiss). One deferral still open: a reattach replays scrollback and can raise old views into the set — to be reconciled next. |
| 2026-09-15 (a group that leaves the stage is dormant, not gone) | Slice 2 of pane-group restoration. A gutter domain switch ran an orphan sweep that DISPOSED every non-primary pane — the image viewer, its tags, any split — engine and mount, permanently; only the three primaries survived off-stage. That is the "lost panes" defect: nothing to return to. Now the sweep first SNAPSHOTS each leaving link group into a dormant set (`app/dormant.ts`), then disposes its live panes. A snapshot is not the pane; it is what a replay needs — the series or volume the group regarded (its stable id, one series is one card), the image view state (layout, slice, window/level, colormap, ghost, now exposed through `state_get`), the member kinds, and a small raster of the viewer. The set is LRU-capped (24, a named constant) so a long session stays legible, evicting only the least-recently-dormant; a group is forgotten only by an explicit dismiss, never by navigating. It persists to the surface's own localStorage and rehydrates DORMANT on reload — the cards come back, never the stage (the scrollback-replay interaction is deferred to the PANES slice). No visible surface yet: the next slice is the PANES-06 domain that shows the set as cards and restores a group. Verified live (a viewer switched away from is dormant with its anchor, faithful view state and thumbnail, not destroyed) and in a unit for the registry (LRU, persistence, dismiss, bad-storage). |
| 2026-09-15 (a PACS viewer is its series' group) | First slice of pane-group restoration (design: pane-group-restoration.md). Opening an image from PACS spawned the viewer with no host group — an anonymous loner the pane drawer could not reach, and a second IMAGE on the same series spawned a duplicate. A viewer opened from a pane that has a group (a files row) already joined that group; the PACS path did not, because it opens with no originating pane. Now that path anchors the viewer's OWN group on the series it shows (the group's regard address is the series folder, `dicom.series`), so the group IS that series' group: the drawer reaches it, and a viewer already on stage regarding the same series is reused rather than duplicated. One series is one group, whichever PACS pane spawned it. This is the foundation for the group as the unit of restoration — a group will be a replayable transcript of the intents that built it, retained dormant when it leaves the stage and brought back from a PANES gutter view, so a navigation away never loses the arrangement. Later slices carry the dormant lifecycle, the PANES domain, and a DIR row verb. |
| 2026-09-15 (a text surface shows the image, not only its facts) | Following on from image becoming a kernel command: a TTY sharing the session saw the series' facts but no picture, so the reflection now carries a thumbnail of the middle slice. The kernel reads that slice's pixels (`dicomSlice_gray` in salsa, uncompressed transfer syntaxes via the dcmjs it already loads), applies the file's window/level or an auto range, and normalises to 8-bit with MONOCHROME1 inverted so bright always reads high. brasa box-downsamples that grid to a cell width — correcting for the ~2:1 aspect of a character cell so the image is not stretched — and renders it one of two ways, chosen by a new declared surface capability, `color`: an ASCII ramp (` .:-=+*#%@`) that any pipe can print, or ANSI truecolour half-blocks (`▀`, two pixel rows per cell) on a colour terminal. ARGUS declares `color: true` in its attach, because the DOM console renders ANSI; chell reads its own terminal through chalk; a bare pipe gets the ramp. Compressed pixels (the JPEG family, RLE) need a codec the kernel does not carry, so the reflection says so in one line rather than pulling wasm into the kernel; a slice that cannot be read at all leaves the facts and no picture. The plain ramp is always the floor. Verified live on the isolated daemon — the ASCII ramp on a piped chell, truecolour half-blocks on a colour one — and in units (`salsa` decode/window-level, `brasa` the two renderings and the compressed and unreadable paths). |
| 2026-09-15 (image is a kernel command, and the surface renders its intent) | The operator, firmly, twice: "There can never be ARGUS-only verbs per se. `image` MUST be a brasa command. I should be able to have a tty terminal and a web browser surface sharing the same calypso and type in the tty `image ~/uploads/sag-anon` and it should work." It was drifting the other way — `image` had begun to grow surface-only help and no place in the kernel, the exact link violation the workspace model forbids. So `image <path>` is a kernel command now (`packages/brasa/src/builtins/res/image.ts`): it resolves what the path is — a series folder, a `.dcm`, or a NIfTI/MGZ volume — and emits a typed `image.view` intent (`IMAGE_MODEL_KINDS.view`, schema in `@fnndsc/menu`) that every surface renders in its own way. ARGUS opens a rendered pane from the intent (`envelope_observe` calls `image_open`); a TTY prints the reflection the command renders, which for a series is the same facts `dcm series` shows under an `image` line, so the terminal answers the identical `image` with text where the browser answers with pixels. The surface keeps only the subverbs that drive a pane already on the field — `image layout|slice|wl|colormap|save|tags|load|guard|ghost` — declared in `SHARED_SUBJECTS` so a bare `image <path>`, `image --help`, and `image` alone all fall through to the session; the surface no longer shadows the command (`surface-never-shadows-the-session`). Help lives in the kernel's own registry, so `image --help` reads on a TTY and in the console alike. Verified live on the isolated daemon and in smoke (`image-answers`): a typed `image <path>` opens the pane through the intent, a bad path is refused in the console by the kernel, and a brasa unit covers the command's grammar and reflection. The TTY reflection is a line of facts today; an ASCII rendering is noted for later. Law: `image-is-a-kernel-command`. |
| 2026-09-15 (the console reads clearly, and the voice is the original again) | Two small nits. The typed line in the console was bold — `font-weight: 700` on both the input line and the input — and at that size with letter-spacing it smeared; the operator read it as out of focus. It is regular weight now, the glyph with it. And the four voices: the operator found the synthesised beeps poor and wanted the originals. TheLCARS.com's template is free to USE but its EULA forbids REDISTRIBUTION, which is why the beeps were synthesised in the first place and never committed. So the original model is restored rather than the files: `scripts/sounds_import.mjs` extracts `beep1..4.mp3` from the operator's own `LCARS-26.zip` into `public/sounds/` (gitignored), and each `<audio>` prefers the `.mp3` and falls back to the committed synthesised `.wav`, so a clone without the zip still answers a press. The sounds moved from a bundled import to `public/` for exactly that fallback — the browser can fall from one `<source>` to the next, a hashed import cannot. Both canons re-recorded: the input weight moved on three elements and the audio gained its `<source>` children, nothing else. |
| 2026-09-15 (a slice sweeps through the volume) | The operator asked for a mode where a fully-contrast slice moves through the 3D volume with the rest a ghost — "slicing through, seeing each slice as it moves." Built as a 4th layout, SLAB, alongside SINGLE / MPR / 3D. The crisp element is the real acquisition slice, a `vtkImageSlice` plane at its true depth, windowed by the pane's own W/L; the rest is a translucent volume render, faint by default (`image ghost <0..1>|off`, ~0.12). The wheel sweeps the slice through the volume, left-drag rotates, the bar reads `SLAB · z n / m`. It is the one layout that does NOT ride a Cornerstone viewport: a `vtkImageSlice` composited into a Cornerstone `VOLUME_3D` viewport crashes that viewport's z-buffer pass (a null texture) in the pane, though the same two actors compose cleanly in a plain vtk renderer — proven first in a throwaway prototype, then localized precisely (the ghost volume alone rendered; adding the slice actor crashed). So SLAB owns its scene (`slabScene.ts`): a `GenericRenderWindow`, a ghost volume actor and a crisp image-slice actor, a trackball for rotate/zoom/pan, the wheel bound to the sweep. Cornerstone still loads the DICOM; the pixels are copied into one `vtkImageData` via `voxelManager.getCompleteScalarDataArray()` and handed over. SLAB is a volume layout, LOAD-guarded like MPR/3D. Verified live on a sagittal T1 and in smoke (`image-slab`). |
| 2026-09-14 (a tool works on the layout it belongs to) | The operator, in 3D: "the zoom and pan buttons don't work, the only control is rotate"; then, of MPR: "probe and measure don't work either." One cause underneath both. The frame carries six tools, but a layout only offers the ones that make sense on it, and the code never said which — so `tool_set` reached for a tool the layout's group had never added and threw, and every button but the default did nothing. Now each layout declares what it offers (`toolsOffered_get`): a stack takes all six; an MPR plane is a slice too, so it takes the pointer tools and the navigators; a 3D render is a projection with no slice to measure, so it offers zoom and pan alone. The panel dims and disables the rest (`rail-na`, line-through) rather than pretending. In 3D, rotate, zoom and pan ride the three mouse buttons at once and the ZOOM/PAN blocks move one onto the primary drag, rotate never lost — and the binding passivates before it rebinds, because Cornerstone's `setToolActive` ADDS a mouse binding rather than moving it, which is why PAN lit and did nothing until now. In MPR the crosshair rests on the primary with no block lit; a pointer tool takes it, and clicking the lit block hands it back to the crosshair. The probe reads the volume the plane samples, chosen by the viewport's own class, since an orthographic viewport answers `getCurrentImageId` too and the method alone cannot tell a plane from a stack. Length and angle draw on a plane and persist. Verified live and in smoke (`image-tools`): a length drawn across an MPR plane leaves a measurement, the probe reads a voxel on a plane, 3D marks the slice tools unavailable and PAN drags the render. |
| 2026-09-14 (the count clears, and the console keeps the story) | Two things after a pull, from the operator: "the 1 RUNNING spinner in the PACS Q/R pane never stops, even clearly after the image has been pulled" and "the console doesn't echo the PACS Query/PACS Retrieve". The count first: a pull reports each series to the status bar under `pull:<seriesUID>` and closes the whole operation under `pull:` with no item id, so the closing message never matched the keys it was meant to clear — and a per-series retrieve carries its ending in `status` (done, unconfirmed, error), never in `phase`, so watching the phase alone never saw a series finish. RUNNING sat at one forever. The reconciler is pure now (`running_reconcile`, six unit cases): an item clears when its status turns terminal, and an operation saying complete or failed clears every key it owns, id or no id. The same fix in the console cascade's `opsActive`. The echo second: a query and a retrieve are big auditable actions the operator takes through a button, and the console is the session's audit — but they ran silently, so the transcript told only half the story (an image opened this way did leave its line, through `line_note`). They are echoed now, as the command lines they are (`line_echo`, the prompt glyph and the line, no submit), then still run silently so a long retrieve never locks the prompt. The incidental probes a pane fires — `pacs list` for the server strip, `mkdir ~/gather` — stay silent; only `pacs query` and `pull` are echoed. Verified live: RUNNING rose and fell to empty on a real retrieve, and `pacs query PatientID:…` and `pull /net/pacs/…` stood in the transcript. |
| 2026-09-14 (3D renders from every layout, not only from single) | The operator: "I viewed a series -- all good. MPR good. I selected 3D ... nada." Reproduced exactly, and it was the path that mattered: single-to-3D drew a volume, single-to-MPR-to-3D drew a black field. The volume is cached under the engine's own id, so the MPR build loaded it there; switching to 3D cleared the rendering engine but not the cache, `createAndCacheVolume` handed back the already-loaded volume, `load()` was a no-op, and the 3D viewport — which first rendered off the streaming load's events — never got one to render on. A first 3D worked only because it built the volume itself. The volume is now purged before each build, so every layout streams and renders as the first does, and the 3D camera is framed explicitly rather than left to those events. Measured on the operator's T1SAG: the render carried zero lit pixels before, 365k after, identical to the single-to-3D path. A smoke check in `image-verbs` reaches 3D through MPR and fails on a black render — confirmed to bite by reverting the fix. |
| 2026-09-14 (the join is round, and the caps clear the fillet) | The operator, looking at the pulse: "I noticed now for the first time just by looking at the pulse that the frame join on the top right of the panes is actually curved. But most of the time it looks square." Measured: the strip starts at the frame's top beneath the elbow with a square corner, the elbow's outer corner is a 22px arc, and only the strip breathes (0.45 to 1.6). So at every phase but unity the strip's corner showed past the arc — dark on the dim swing, bright on the lift — and at unity the two merged into the square silhouette the operator had been seeing all along; the curve was there the whole time and visible only when the breath separated the pieces. Frozen at 1.6 the bright square is plain in a 5× crop. The strip now wears the elbow's own radius at its top-right, so no pixel of it lies outside the arc at any brightness. The second thing in the same screenshot, "SERVER is close to the right edge, there is a notch out of the panel": the elbow reaches 36px into the field past the spine, its fillet is a black square with one corner rounded, painted OVER the field, and the patient-level caps row ran to the field's edge — so the SERVER cap lost a 30×13 corner to the fillet, which is the notch, and its label crowded what edge was left. The reach is now declared once (`--elbow-clear`) and every roster grid — caps, form, rows — keeps it clear on the right, uniformly, so the levels stay aligned and nothing sits under the fillet. Two smoke checks in `pacs-listing`; both canons re-recorded. Recording them turned up a third thing: the old canons were taken at 1297 CSS px of height — whatever one chromium build's window chrome left of 1440 — so a hundred heights scored as moved on a machine whose chrome differed, none of them CSS. The smoke driver now pins the viewport at 2560×1440 at every pixel ratio, and the canon README says the state a canon is recorded in (no cached PACS answer, no host lamp, pinned viewport). Isolated on one daemon with the change stashed and unstashed, the CSS moved exactly one property: the strip's radius. Two things seen and left: the elbow's own column does not breathe while the strip beneath it does, a seam at the elbow's foot by the same mechanism, which is the design saying what is pressable, and the image field's top-right corner also lies under the fillet, unnoticed on a black corner. |
| 2026-09-14 (the field keeps its shape, and a stack is whole before it turns) | Four nits from the operator on the image pane, and one of them was a defect of the first order: opening the mode frame narrowed the field and the image "smushed" — horizontally compressed to fit — with every PROBE ring and LENGTH line left where it was. The library sizes its canvas once, at enable; the pane's CSS then stretched that canvas into the narrower box, so the pixels squeezed while the annotations, drawn in world coordinates by a camera that had not moved, stayed put. A `ResizeObserver` on the field now resizes the engine with the camera kept, and the shape is the shape: canvas backing pixels equal the field's CSS width times the device pixel ratio after the frame opens, which is what the smoke measures. PROBE was the library's ring: press, a circle appears, nothing is said, the circle lingers. The operator: "I should imagine a PROBE would have a live/running display of voxel position and intensity value." It is that now — a readout at the foot of the field, `X i  Y j  ·  SLICE k  ·  value HU`, while the pointer is on the image, and nothing on the image after. A probe is a readout, not a mark. The stack scrolled in pieces: slices rendered as they landed and the wheel found gaps. The operator's rule, "scrolling should only be allowed once all slices are available", is now the engine's: after the first slice the rest are counted in, `LOADING n OF m` on the bar and `READING SLICES n / m · nn%` on the field, and the wheel is bound only when the last one lands. The guard covers the stack as well — decided with the operator against always fetching everything — so over 256 MB the first slice shows, the bar reads `SERIES <size> · LOAD FOR STACK`, and LOAD fetches the rest; `series-loads-by-consent` is amended to say so. And the header overlay leads with `SLICE n OF m`, following the wheel, since a count is the one line of a header that changes. Laws: `a-field-keeps-its-shape`, `series-loads-by-consent` (amended). |
| 2026-09-14 (a press is answered before the kernel is) | The operator, timing it: "Hitting the IMAGE button shows often NO REACTION for upwards of 4 seconds — I counted." The pane was spawned only after `dcm series` had answered, and that ask is a header read over a wire against a series the kernel has never seen; four seconds of a surface that has not moved is four seconds in which the press looks lost. Now `image <path>` puts the pane on stage first, `OPENING <name>` standing on the field with the pacing bar and `OPENING` on the bar, and the engine's own notice (`READING n SLICES`, `BUILDING MPR`) takes over when the series arrives. The other half of the operator's sentence: "if this data is multiple files, then a counter % is indicated" — counted work now says `done / total · nn%`, while a stack still costs one file by consent (`series-loads-by-consent`), so the percentage belongs to the volume builds that pull every slice. What could not open is no longer a console line alone: the notice turns to `NOT A READABLE SERIES` over the path that was asked for, no track (a failure is not work in progress), and the bar says the same, so a pane that opened for a press never stands blank. Law: `a-press-answers-at-once`. |
| 2026-09-14 (a series that has landed can be seen) | Two knee series pulled, both rows reading `✓ 1 IN CUBE`, neither offering IMAGE — the operator's screenshot, and the operator's rule: "Once an image is available, it must be viewable, surfaced by the PILL appearing." The row's verb rule was right and its facts were stale: IMAGE waits on `folderKnown`, and the retrieve channel's "done" carries a count and no place, because LONK confirms by count and CUBE files the folder a beat later; the surface only learned the folder from the next query. The old comment on `seriesVerbs_offered` said as much and called it a deferral. It is not one now. The kernel is the only party that can name the folder, so the kernel says it: `pull` resolves each landed series' storage before its channel closes (the same resolver the query uses, with the same retry against CUBE's registration lag) and emits one more per-series `done` carrying `path`, a new optional field on the wire's progress message. The row takes the folder from that message, and `seriesPulled_mark` now changes a row already marked home when the place arrives after the count. No surface asks CUBE anything on its own account, which is the law that ruled out the other fix. A folder CUBE cannot yet name is left for the next query, as before. Verified by unit on both sides of the wire and live against the hospital PACS. |
| 2026-09-13 (the frame clears AA, and two of the three defects were patterns) | Measured every colour pairing in the frame that carries type against WCAG 2.1 AA, per scheme, with the type sizes read out of the canon so each pair met the threshold that actually applies to it — 3:1 for the 22px bold gutter labels, 4.5:1 for the 15.4px about-face caps. The result inverted the expectation: the Star Trek palettes were the accessible ones and the CLINICAL palette was the offender, which PHAROS inherits wholesale because it re-seats shape, type and ornament but keeps MEDICAL's seven colours. Three defects, all now clear. The clinical navy carried black type at 2.35:1 and is lightened to `#30619d` for 3.32:1, the margin over the bar being deliberate so a later nudge cannot silently re-break it. The INERT BLOCKS were the instructive one: `opacity: .5` faded the ground toward the page's black while the label, already black, did not move, so the two closed on each other at 1.36:1 — and the first fix, fading the ground alone, changed nothing for exactly the same reason. An unavailable block now INVERTS rather than fades: ground halfway to black, label to the scheme's `--butter`, 4.6:1 at worst, still obviously unavailable from the drop in weight beside its live neighbours. The about-face caps invert the same way but only under the clinical schemes, because a blanket light label would have broken GOLD, whose ground is the lightest of the five — the general law being that *on a themed surface a contrast fix that is not theme-aware trades one failure for another*. One correction worth keeping: an earlier draft of the audit recorded `--pumpkin-pie` failing as a "status wash", a pairing the measurement invented — it paints a progress track and never carries type. A contrast table is only as honest as its list of pairings. Recorded in `docs/WCAG-AA.adoc`; both canons re-recorded. |
| 2026-09-11 (the fingerprint sees what draws the frame) | Said by the operator, and it ends a question I had wrongly put to him as a choice: "This must be fixed. I don't care what's locked or not." The canon read elements only — `getComputedStyle(el)`, no second argument — so every `::before` and `::after` in the frame was outside it. The rebuild dropped SEVEN of them and both canons scored zero the whole time: the mask behind FILES-01's swept corner, the inner elbow of each pane (two apiece), and the spur and bite carried by the long block of each band. Three were found by an operator looking at the surface; the other four named themselves the moment the fingerprint was widened. The record now covers each element and both of its pseudo-elements, 285 records against 273 elements, and watches the properties that actually draw the shapes — `content`, `background-image`, `clip-path`, `mask-image`, `filter`, `transform`, `animation-name` — none of which were in the original 55, though the elbow's fillet is a background-image, PHAROS cuts every stone with clip-path and the nameplate is a mask. Two supports were needed to make it trustworthy: every animation and transition is stopped before the snapshot, because the chrome breathes and `filter` otherwise records the moment rather than the stylesheet; and a url() has its origin stripped, because the daemon's port is not a look. Both baselines re-recorded against the frame as it stood before the rebuild, and what remains is 19 properties, every one of them either a subpixel rounding from authoring a percentage where the template wrote a pixel, or the corner mask deliberately moved onto the elastic column where it belongs. The standing rule is unchanged in spirit and stronger in fact: zero differences gates a frame change, and a human still cycles every scheme with the console shut, because one viewport and one session state is all a fingerprint ever sees. |
| 2026-09-11 (an elbow has two halves, and the fingerprint sees neither) | The operator caught what the canon could not: the inner elbow was missing from both panes, so the bar met the gutter in a hard T where the frame had always turned. Finding it took a pixel diff; fixing it took measuring the frame as it stood, twice, because the first attempt was wrong in an instructive way. The inner elbow is TWO pseudo-elements in one 60px square sat flush against the bar: `::before` lays a hard-split diagonal — half `--october-sunset`, half page, 50% stops, no blend — and `::after` drops a square of page over it with the corner facing the turn rounded away, leaving the fillet. Carrying only `::after` reproduced its computed values exactly and drew nothing, because a black square over black is invisible; the piece that makes the shape is the one underneath. Together with the corner mask on FILES-01, that is four pseudo-elements the clean-room rebuild dropped while both canons read zero differences, and it settles the standing of the gate: `getComputedStyle` is called on elements, so ANY mask, fill, band or ring drawn with `::before`/`::after` is outside the fingerprint entirely. A frame change is checked by cycling every scheme with the console shut and diffing the pixels, and the canon is what catches the rest. One method note worth keeping: the chrome breathes, so a screenshot comparison measures the shutter unless every animation and transition is stopped first — with them frozen the joint matches the old frame at zero differing pixels on GOLD, MEDICAL and NEMESIS, and eight subpixels on PHAROS, which squares the corner by design. |
| 2026-09-11 (PHAROS speaks in lower case) | PHAROS now sets every label it owns in LOWER CASE. The imported look shouts — every label in caps — and the caps are half of what makes it read as a starship, so lowering them is the same move as swapping the face, carried to the letters themselves. The line it draws is the one the faces already drew: *the frame may re-case its own words; the session's words are never re-cased.* A listing cell, a console line, a typed field and a pipeline's title keep the case they arrived in, because `README.md` is not `readme.md`, and the footer's attribution keeps its case because a name is being credited and *TheLCARS.com* is how that name is spelled. Form controls had to be named one by one: a browser hands every button `text-transform: none`, so inheritance never reaches a gutter block, a column cap or a capsule. Verified by asking the surface rather than by looking at it — the computed transform of eleven representative elements, under both themes: LCARS did not move by one property, and under PHAROS the chrome reads `lowercase` while every content region reads `none`. The canon records exactly the 279 properties that moved.
| 2026-09-11 (a scheme with no name still needs a palette) | LOWER DECKS is selected by REMOVING `data-theme`, which makes the bare root its only seat. Its seven colours arrived with the imported stylesheet, so when that stopped shipping the scheme rendered as an absence: every block still drawn, every colour resolving to nothing, black on black with the type floating. It survived review because nothing looks at it — the surface boots on MEDICAL, and the fingerprint cannot see a property that no rule sets. Measured rather than guessed, and twice: the values in the template's own file are the values the old build painted, the gutter reading rgb(255,119,0) for `--orange`. Seating the seven restores eleven, since the four state hues derive from them. What the sweep also settled is the SIZE of the loss: all 36 tokens the frame reads, under every scheme, across a build from before the clean room and one from after — GOLD, MEDICAL, NEMESIS and PHAROS came through the rebuild identical, and LOWER DECKS was the whole of the damage. The law to carry forward is that *a theme selected by absence has to be declared somewhere that absence still reaches*, and the check that finds it is cycling every scheme with the console shut, not a diff. |
| 2026-09-11 (ARGUS carries its own font and its own voice) | The frame was ours by the morning; two borrowed assets were not. The TYPEFACE turned out never to have been the template's: Antonio is the Antonio Project's under the SIL Open Font License, and the template merely carried a copy — so ARGUS vendors it from the source, licence alongside, exactly as PHAROS vendors Chakra Petch. Checked the obvious way, by measuring: the canon did not move by one property, because it is the same face. The SOUNDS were the template's, and there is no open equivalent to reach for, so they are synthesised instead — sine partials under an exponential decay, written straight into a WAV by `scripts/sounds_make.mjs`, with a short fade at each edge because a waveform that begins at a non-zero value clicks and a click is the one sound nobody designed. Four of them, saying four different things: a press acknowledged, a thing arriving, a retreat that FALLS a whole tone because that is what makes it read as away, and a refusal that is low and flat and over before it can be enjoyed. What follows is the point: `theme_ensure.mjs` is gone, `src/lcars/theme/` is gone, the gitignore entry is gone, and the build has no materialization step. There is no zip, no stand-in, and nothing to degrade to — which also resolves principle violation 6 by removal rather than by mitigation, since a page can no longer be showing a stand-in nobody announced. Both themes verified identical after every step. The attribution stays, and the footer's wording is now more accurate than when it was written: inspired, and adapted. |
| 2026-09-11 (the frame is ours, and provably) | ARGUS's page frame no longer comes from TheLCARS.com's stylesheet. It is written in `argus.css` from `tests/smoke/canon/lcars.json` — the computed style of our own rendered page — and the import is gone. That distinction is the point: nothing was transcribed from the template's source, so the provenance is measurement of our own surface rather than derivation from someone else's file, which is what makes it a clean room and not a fork. The gate was the canon, and it was unforgiving in the way a gate should be: 2,356 differences at the start, then 714, 512, 442, 346, 284, 228, 120, 101, 95, 84, 59, 31, 7, and zero — identical across 275 elements and 55 properties, under BOTH themes, and identical again with the template's file emptied entirely. Four mistakes were caught that no eye would have found. A generic `button[class^='panel-']` outranked the per-panel rules by specificity and quietly repainted the gutter. The colour tokens were guessed rather than looked up, so `--mars` is red and not the frame's dark blue. The first band's offset is a share of the VIEWPORT, `2.5vh`, and a rem value that looked equivalent was 17px short and cascaded through every height above it. And `--panel-border`, which existing rules already asked for, had only ever been DEFINED in the template — so the console and language blocks silently lost their rule the moment it was withheld, which is the cleanest example of the borrowing this work was for. One planning error, recorded because it changed the method: the slices were meant to land one at a time, but a frame written alongside the template it replaces is a frame neither one thing nor the other — 291 properties moved while both sheets were loaded. The two had to change together, which is why this lands as one commit. What the operator's zip still supplies is the TYPEFACE, named by the frame and absent without complaint. |
| 2026-09-11 (ARGUS's own look is the canon) | Said by the operator, and it settles a question that had been open since PHAROS: "the current look-n-feel of ARGUS, i.e. its re-implementation of theLCARS.com is our canon. That is our look." The drift is real and it is ours — TheLCARS.com sets the left gutter to 240px and ARGUS sets it to 168, a 30 percent narrowing chosen against panes with seven columns in them, and the template's proportions were never chosen for that. So the frame ARGUS wears is already our design wearing their colours, and the clean-room ahead is not a reimplementation of somebody else's template but writing down what we already diverged into. What locks it is `tests/smoke/canon/`: the computed style of every chrome element, 276 of them across 55 properties, taken under both themes and committed. A screenshot cannot prove two pixels did not move; the browser's own answer to what a thing looks like, after every sheet has had its say, can. `themeSnapshot.mjs` takes it and diffs it property by property, and the gate for any work that touches the frame is zero differences. Two things had to be excluded to make the fingerprint stable rather than merely detailed: the console transcript and the listing rows, which are content and differ because the session did different things, and the latency readout, which is present or absent depending on the wire. Verified: two consecutive runs agree on all 15,180 values. |
| 2026-09-10 (a control nobody can see is not there) | The operator, twice: the PACS answer's EXPORT CSV "STILL IS NOT THERE". It was there, and that is the lesson. It had been moved onto the results frame — the right place by scope, on the field it exports, beside FILTER — and the results frame IS the closed spine until somebody opens it. Measured: 22px wide with its label clipped to nothing. FILTER survives that because an operator already knows FILTER lives there; a control introduced into a place nobody opens is a control that was never added. It stands on the query line now, beside the question that produced it, 119px and readable without a gesture. The spine itself went the other way: it is a CONTROL, so it grew from 8px to 21px in two passes and its pulse now swings 0.45 to 1.60, because the same swing that is plain on a wide lid is invisible on a strip. The smoke check moved with the control, and it no longer asks WHERE the export sits — that was the assertion that passed while the operator could not see it — but whether it is wide enough to read. |
| 2026-09-10 (a preview asks the session, not just the route) | A package's files under `/usr/share/packages` listed correctly and previewed blank. The preview reads a file's head over the `/vfs` route, and that route serves what CUBE STORES: a file a VFS provider MAKES — a package manifest, a readout under `/proc` — is perfectly real to the session and unknown to the route, which answered 404 and left the tile empty. A file that reads fine in the console looked like an empty file in the pane, which is the specific failure the surface has now been caught by twice: a blank is the one answer that says nothing. The preview falls back to the session's own read, which can read anything it can list, and carries a refusal through rather than swallowing it. Verified on a package folder: the manifest, the description, the name and the authors all draw. |
| 2026-09-10 (clickable chrome breathes) | LCARS chrome is flat colour, and a flat block that ACTS looks exactly like a flat block that DECORATES — an operator preparing a demo asked for the difference to be visible. The pieces that can be pressed now breathe: a slow, shallow lift in brightness, 2.6 seconds and about 16 percent, on the console's lid, the header's restore strip, the mode strip and the mode fill. Shallow on purpose, since a pulse that draws the eye competes with the data the eye is meant to be on; measured moving between 1.06 and 1.15. Hover stops it, because hover is a stronger statement of the same fact and the two would otherwise fight over one property. Reduced motion turns all of it off, the image pane's pacing bar included: motion is a preference, and a surface that ignores it is one nobody can sit in front of all day. |
| 2026-09-10 (a run follows itself) | The operator, after starting a plugin: "when a new run happens, I'd expect telemetry to appear in the header". It did not, and the reason was worse than a missing push. The heartbeat is a second long and was working; what it reported was a cache that never learned. A new feed enters the cache with its jobs SCHEDULED, which is what they are at that instant, and nothing revisited them unless a pane happened to be open on that feed — so the operator's own run never turned over to running, never turned over to finished, and RUNNING sat at zero while SCHEDULED only ever climbed. That operator's header read `SCHEDULED 90` with nothing running: not a gauge, a tally of everything they had ever started. The sampler already existed and already knew when to stop, since a settled feed cannot change; what was missing was anyone starting it when nobody was looking. A run now takes out its own watch, released when the feed settles. Measured live: RUNNING went 0, then 1 IN 1 FEEDS while the root ran, 1 again for the child, and back to 0 — and SCHEDULED rose by one and came back down instead of staying up. Where the watch is taken matters and was got wrong first: putting it in the cache-writing helper made every test that records a feed start visiting CUBE, which is the helper surprising its callers. A run is followed where a run is STARTED. The historical backlog those 90 represent is NOT fixed by this and is its own defect: entries stranded as scheduled before anything followed them. |
| 2026-09-10 (a series is known by its name or its place) | The operator had a folder of DICOM files of their own and no way to open it from the browser, though `image ~/upload/SAG-anon` opened all 192 slices from the console the whole time. The verb was offered on a NAME alone — oxidicom writes a series folder as `<number>-<description>-<7 hex>` and nothing else does — which is a good signal and an incomplete one. Two things were put aside on the operator's call. Offering IMAGE on every folder, because a verb meaningful in a handful of cases is noise in the rest: "I don't like adding a verb to every selection that is meaningful in only a handful of cases." And looking ahead into each folder to decide, because that is one request per row before the operator has pressed anything — the same cost that made a replayed PACS query feel like a fresh one earlier the same day. What was taken instead is the operator's own: inside `/SERVICES/PACS` the PLACE says what a folder is, and depth is free to read because the row already holds its path. So a series is known by its name anywhere, or by sitting at series depth in the PACS tree, whatever it is called there. Verified live against the hospital tree: a series row inside a study offers IMAGE, a patient row at the server level does not. The folder outside the tree still has no verb, which is the trade the operator chose. |
| 2026-09-10 (a dive that opens nothing strands the diver) | The operator entered a node and got what they read as a crash screen: the pane filled with one flat colour, nothing moving, nothing to press. Nothing had crashed. Entering a node flies the camera to just shy of the node's surface — arrival is meant to read as passing inside — and the arrival callback then mounts the node's own filesystem over the scene. If that second half declines, it declined SILENTLY, and a silent decline is not a cancelled dive: the camera is already inside, the scene is held so even the idle spin stops, and the operator is left looking at the inside of a sphere with no way out. Two ways in existed. The pane may hold a record of an overlay whose DOM went with a rebuilt pane, a ghost that then refused every later dive on that pane; ghosts are dropped now rather than believed. And a decline for any other reason now flies the camera back and says on the console why. Esc is also a way out from anywhere: a camera held inside a node with nothing over it answers Escape by flying home, whatever left it there. The operator's third question was the sharpest — why no test found it. Because none had ever dived. The files pane's inline diagram has a dive of its own and the smoke checks it, which made the gesture LOOK covered; that one immerses a facts chip and never mounts a filesystem, so the DAG pane's dive, the whole second half of the animation, had no check at all. It has one now: touch a node, dive, and assert the node's own listing arrived and that Esc leaves the node while keeping the scene. |
| 2026-09-10 (a rebuilt surface has to arrive) | The operator restarted the daemon after a merge and reported the new control simply absent: "I see no ANON pill anywhere on the PACS Q/R?". It was in the built bundle; it was not in their browser. The daemon served the web root with a content type and NOTHING else — no `cache-control`, no `etag`, no `last-modified` — and silence there is not neutral: a browser is then free to guess, and it guesses that it may keep the page. `index.html` is the file that NAMES the hashed assets, so a kept copy pins the whole surface to the build that wrote it, and every rebuild after that arrives nowhere with nothing on either side saying why. The bundle is two kinds of file with opposite needs and now says so: `index.html` is `no-store`, because it is small, it is the index of everything else, and it must never be the stale part; a hashed asset is `immutable` for a year, because a different build is a different URL and this copy is therefore good forever; anything else revalidates, which is the safe answer for a name that can be reused. Verified on the wire, both headers read back from a running daemon. The lesson is the general one: shipping a surface is not the same as delivering it, and the delivery had never been tested. |
| 2026-09-10 (a column of bars is read as a column) | Operator review of the series level, from a screenshot: the progress bars were "jagged on right". They were, and the cause was in the cell rather than in the bar — the bar and its state note shared one flexible track, so every bar stopped where its own note began, and `NOT RETRIEVED` beside `✓ 74 IN CUBE` tore the right edge into steps. The note has a track of its own width now and every bar ends on one line, measured across 21 series. That width is measured rather than guessed in both directions: the longest thing a series ever says is a retrieve in flight, `RETRIEVING 1410/1760` at 120px, so a track cut to the resting states would clip the note at the one moment an operator is watching it — and everything past that is bar the operator does not get, so it is not rounded up generously either. Two more from the same review. A count is read against the counts above and below it, so FILES is set flush right and its cap follows it there. And EXPORT CSV sat in the GATHER strip, where SAVE and CREATE FEED both act on the cohort, while it alone acted on the whole answer — placement saying the opposite of what the control did. There are two now, which is what the operator wanted once the ambiguity was named: the answer's export stands on the results frame beside FILTER, on the field it exports, and the cohort gets `EXPORT GATHER CSV` beside the cohort's own verbs, naming its narrower scope and composed the way SAVE composes its manifest. Three smoke checks hold the geometry, since a torn edge is exactly the kind of thing every test stays green through. |
| 2026-09-10 (a demo need not name a patient) | Demonstrating against a live hospital PACS puts a real name, record number and date of birth on a projector, and the operator asked for a way to query without that. ANON, a pill beside the command line, arms a kernel that stands in for every identifying value before an answer leaves it: the record number and accession become salted tokens, the name becomes a stand-in name of the same DICOM shape, a study date falls back to the first of its month and a birth date to its year. Stood in for on the DECODED payload, which the model, the table, any CSV and the console text are all built from, so one substitution reaches every one of them and none of them has to know. The salt is made once per daemon and never written down: a record number is seven digits and an unsalted digest of that space is enumerable in under a second, so tokens are stable through one demo, meaningless after it, and a pairing leaked from a recording says nothing about any other. The DICOM identifiers are deliberately left real — they are never shown, and they are what the held-state reconciliation matches on, so standing in for them would break an answer while hiding nothing. Three findings shaped the surface. The typed line is the first identifying thing on a screen, so the pill masks the form fields AND the command line, which spells the same values back; the line still reads exactly what runs, keeping the pane's standing claim, it is simply not readable across a room. Masking a field an operator is filling hides their own typos, and a query put to a hospital PACS for the wrong record number is worse than a visible one, so REVEAL lifts it. And the pill arms the NEXT query rather than toggling a view, because the substitution happens in the kernel: rows already on stage were fetched one way or the other and cannot be turned, so arming clears the answer instead of leaving rows whose state nobody could name. What it does not cover, said plainly: the files browser after a pull still shows CUBE's own folder name, which carries the record number, the name and the date of birth. |
| 2026-09-10 (a replay asks by study, not by series) | The operator, on a historical MRN: it "took 15+ seconds .. that is NOT what a cached restore should be and felt 100% as if it searched the PACS again". It had not searched the PACS. The stored answer came back at once and its date was honest; what took the time was the reconciliation that follows — asking CUBE, for EVERY series in the answer, whether it already holds it, at two round trips each. Measured live on a patient's history of 299 series in 20 studies: 20.6 s, and 6.8 s for a 14-series accession, with the whole answer held back until the last of them replied. So the one thing a replay exists to avoid, trouble on the wire, is what a replay spent its time on. CUBE indexes a stored series by its STUDY, so the same reconciliation is one page per study — a count of tens, not of hundreds — and file counts are a second pass over only the series actually home, since the series record carries the folder but no count. The same two questions now cost 3.1 s and 1.5 s at the kernel, measured the same way. The operator's own proposal, a cached mirror of the `/SERVICES/PACS` tree, was put aside for a reason worth recording: `folder_path` is built from names and does not carry the SeriesInstanceUID, so a folder cache cannot answer "is this series held" without the series records anyway — and with no change feed from CUBE, a long-lived mirror of a tree that grows with every pull cannot be invalidated honestly. The records ARE the index; they only had to be asked for in one breath. Held by unit tests that pin the QUESTION COUNT rather than the answer, because an answer built the slow way looks identical. |
| 2026-09-10 (an abandoned question is not a failure) | The operator asked what they see if they `rm` something and then press Esc, and the two answers turned out to be different sentences. `rm -rI` — what the row's DELETE verb runs — caught the rejection and said `nothing removed (1 kept)` in grey, an ordinary outcome; `rm -i` awaited the same question with nothing around it and said `rm: cannot remove 'x': the operator abandoned the question` in red, an error over a file nothing had touched. Under `-i` over a list it was worse than inconsistent: the rejection left the walk, so every file behind the abandoned one was silently never offered. Both paths now treat a question that came back with no answer the way `a-question-is-answered-where-the-session-speaks` already says they should — the command is told, and reports what it did not do. `-i` skips that file by name and stops asking, because an operator who backed out of this question is not waiting to be asked the next nineteen, and it says `2 more not asked about, and kept` rather than leaving a set silently alone. The reason travels onto both lines, so a surface that has quietly lost its voice does not read as an operator who declined — the old `-I` catch swallowed the two cases into one. Verified live on all three shapes, every file still there afterwards, no error line anywhere. Also here: a stray `|||||||` conflict marker had been sitting in this table since #499, drawing a garbage row. |
| 2026-09-09 (bars say what happened, quietly) | Operator review of the RUNS roster. A finished feed drew a full bar in the bright done hue, and most DAGs finish, so the roster read as a wall of bright success — loud where nothing is wrong, drowning the running feeds a live roster is for. The done fill takes the recessive dark tone the ID/owner/created columns wore (`--pumpkin-pie`, a dark blue on MEDICAL), and those columns take the progress shade (`--daybreak`) in trade — so the roster's mass of finished bars sits back and a running feed's brighter bar is what the eye catches. And an errored feed drew a full bar in the error hue, which says nothing a red mark would not; it now fills only to the work that succeeded — `successfulNodes / totalNodes` — in the error hue, so the bar's length is how far the feed got before it died (48738 of 58760 then errored is an 83% red bar, not a full one). The failed count rides the wire as `jobsErrored` on the feed-list model, from the proc cache's own per-status counters, and the fill falls back to the settled count when a daemon does not report it. Verified live: 480 done bars at alpha 0.34, 225 errored each at `round(succeeded/total*100)%`.
| 2026-09-09 (the boot ends at a login) | The daemon face is retired. An interactive `chell --daemon` now ends its boot the way the call-out does: once the daemon is listening, an ordinary `chell --remote --attach` surface is put on the daemon's own terminal, so the boot log runs straight into a prompt. It is a child on the wire (the daemon's sink and surface singletons are never shared), `exit` detaches and Enter attaches again, Ctrl-C at the idle terminal stops the daemon, and the daemon's own writes while a surface holds the terminal are held and handed back on detach. The same slice lifted the boot readout: deferred steps settle as `[ OK ]`/`[FAIL]` instead of an open `[PENDING]`, every count agrees with its noun (`count_noun`), the stack banner writes every package out in full with an ARGUS row (`ARGUS Renders Graphical User Surfaces`, coined here) and drops the welcome, and a daemon bound on every interface advertises its fully qualified name. Settled section: _The daemon console_.
| 2026-09-09 (the lab's pulse, and a readout that acts) | Epic #495, S6. `RUNNING 480 IN 12 FEEDS · SCHEDULED 3` joins the INDEX instrument: the lab's pulse, derived once a second from the roster's own job counters (kept current by the visit delta and the roster sync, so no call is made) and carried on the telemetry heartbeat (`state`). Its ERRORED figure is the one press on the face that acts: a capsule in the errand's hue that opens the runs roster filtered to the feeds with errored jobs (`status:error`, the listing's own filter, which holds across the rows arriving), the pane put on stage first so the press acts on what the operator can see. That needed a law of its own beside `a-control-lives-where-it-acts`: `a-readout-that-acts-says-so` (smoke). The roster filter is now a pane method (`roster_filter`) so RUNS-02's gesture and the press share one path.
| 2026-09-09 (the lane, the beat and CUBE's pace) | Epic #495, S5, from the creative think the operator asked for after the INDEX instrument landed. Three rows join it on the ARGUS WEB face, each changing what the operator does. LANE: the serialized command queue as a gauge — `IDLE`, or the line holding it, for how long, and how many wait behind it, red past ten seconds; the daemon carries it on its telemetry heartbeat (`lane`), counting the queue as commands enter and leave it and naming the running line, an instrument command never counted as holding the lane. This is the gauge the stall that opened the epic had no reading on. BEAT: the age of the last heartbeat on the surface's own clock, red past three seconds, the daemon's health lamp (the heartbeat defect of #484 would have shown at once). CUBE: the pace CUBE serves pages at — the median of the last twenty page fetches, timed as they pass through cumin's one `resource_call` — carried on the heartbeat (`cube`), and the number that turns a walk's page count into time: every FEED row on the INDEX instrument now reads an ETA, from the walk's own observed rate once it has moved and from CUBE's pace with the window of four before that. Smoke `lane-instrument`: IDLE and a fresh beat at rest, IDLE while a feed indexes off the lane, a pace once pages were fetched.
| 2026-09-09 (the roster is index movement) | Epic #495, S4, found while accepting S1 live: with a large topology walk running off the lane, a RUNS-02 press sat BUSY for over ten seconds, because `proc feeds` awaited the roster's sync — the delta, or the ten-minute full walk of every feed the identity can see — inside the foreground command, and CUBE was slow under the concurrent walk. The roster is index movement like the rest: `proc feeds` and `proc jobs list` now answer from the cache at once and START the sync (`procRoster_syncStart`, deduped with a sync in flight); the walk runs off the lane; the prompt context names it while it runs (`procWarmup.roster`, delta or full) so the status line reads ROSTER WALK, the INDEX instrument shows a ROSTER row, and chell's prompt says `[roster: full walk]`; and arrivals land as they did, bringing the pane back for the rest (the roster it answered with may be a few feeds behind for the seconds the walk takes, which the instrument says). The boot-time sync and the restore reconcile still wait, as warm-up steps do.
| 2026-09-09 (four pages in flight) | Epic #495, S3. Measured on the hospital CUBE: about 2.8 s per 100-row page, so a 58,760-node feed walked sequentially for 28 minutes (35 under the smoke suite's load). The per-feed topology walk now fetches four pages at once: the first page alone, since it tells the server's total and so every further offset, then a window of four, each page yielded as it lands. The consumer keys rows by id and commits the whole set when the walk lands, so arrival order is nothing to it; the global sweep keeps its sequential loop, because its resume-by-offset cursor needs pages in order and a contiguous-prefix commit is its own slice. A server that reports no total is walked one page at a time as before. Four is PACS plural's precedent against this CUBE (#400); eight is untested on the shared hospital instance and not tried without a measurement. cumin's `listPages_walkWindowed` is the one pagination loop, windowed, with tests for the window's width, the no-total fallback and a failing page; salsa's `FEED_WALK_IN_FLIGHT` names the width.
| 2026-09-09 (index movement is read on the header) | Epic #495, S2. Index movement was annunciated on the prompt and on the status line as text that vanished when it was done; the operator asked for it to be surfaced clearly in the header pane, the original warm-up included. The INDEX instrument now heads the ARGUS WEB face — the live telemetry beside LINK, JOBS INDEXED and OPS, where the operator corrected a first placement on the resting face: that face carries the versions, and a tracker among version numbers "makes no sense". Permanent: a quiet index reads `95,989 JOBS · 705 FEEDS · CURRENT` (a quiet daemon says so; a blank would be a lie), the global sweep rides that row with a bar and its state, a feed's first-visit walk takes a row of its own (`FEED 2384 · INDEXING 6,500 / 58,760`, a bar that follows the count; `FAILED AT n / m · reason` while the failure is remembered), arrivals a third (`ARRIVED FEED 21 · FEED 22`), and a warm-up step that failed behind the prompt keeps the index row red with the steps named. One source drawn twice: the same prompt context the status line reads, and the telemetry heartbeat's steady counts; the bars are the listing façade's progress idiom in the state hues, never a raw colour. Building it found a gap the full smoke had just shown red: the prompt context carried ONE feed walk (the earliest in flight), so a second walk — the suite's `proc refresh 834` beside the operator's 2384 — was invisible on the status line, on its pane, and on the instrument. The context now carries every walk in flight (`procWarmup.feeds`, earliest first; `feed` stays the first for older readers), and the status line, the pane, the instrument and both chell prompt themes name each one. Seven jsdom tests compose the rows from the context; smoke `header-index` reads CURRENT, watches a feed's row appear with its bar during `proc refresh`, and sees it go. Proving S1's kick live found two more: the pane's repaint path dropped a sampler's graph while the pane was in its pending posture (the landed graph now completes the wait as the answer to the ask), and `proc refresh <feed>` removed the feed's roster row and re-read it from the own-feeds endpoint only, so a feed seen through the public listing came back "not found" after its re-walk (the refresh now evicts the topology and keeps the row, re-reading it from own then public feeds); a refresh also no longer carries the `feed.indexing` model, since a refresh is not a request to view and its answer must not claim a pane.
| 2026-09-09 (index movement never holds the lane) | The measured stall the 2026-08-31 row waited for: entering a never-indexed feed of 58,760 nodes showed the annunciated count warming (6,500/58,760, so the earlier law held) while a PACS query and everything else spun for the minute it took, because the daemon runs every command through one serialized lane and `feed diagram` awaited the whole 588-page walk inline in it. Eight decisions, grilled: the cold walk is index movement, beside the global sweep and the visit delta, not a background-job facility (those stay deferred); `feed diagram`, wire `feed.dag` and `proc refresh <feed>` answer at once with a typed `feed.indexing` envelope on every surface, chell printing one line and its prompt segment carrying the count; a started walk is never cancelled, and a failed one names where it stopped for a minute and retries on the next visit; completion kicks the feed's watch, which holds the floor while pending and never settles on an empty topology, so the graph publishes within one build; the pane shows the walk in place (INDEXING FEED n with the live count, FAILED with REFRESH retrying). The blocking command was removed rather than prioritised, exactly the condition the old row set, so express-lane queue priority stays shelved. Owed in this epic: the header INDEX instrument on the resting face (idle CURRENT, one row per movement with a bar), four pages in flight in the per-feed walk, and — found while accepting this slice live — the roster's ten-minute re-walk inside `proc feeds`, which held the lane for over ten seconds while CUBE served the topology walk (#499). Measured on the hospital CUBE: about 2.8 s per 100-row page, so 58,760 nodes walk in about 28 minutes sequentially, which is what the four-in-flight slice is for. Law `index-movement-never-holds-the-lane` (smoke).
| 2026-09-08 (PHAROS, the house visual language: the lighthouse, flat) | The operator liked PHAROS's first light and asked for it with medical colours. Two builds went wrong before a line was kept, and the record of why is in `pharos.adoc`: a token swap on the imported frame read as the imported look in beige, and a frame without the imported look's mass read as buttons in dead space. The finding is a law of taste rather than of code: the imported look's chrome IS its structure, and a language that subtracts that mass must put an equal mass back. Three studies did (the continuous run chamfered, the reading room's lightbox, the lighthouse's tower); the tower was chosen, flat, since the bevels of the first pass looked obsolete on arrival. The grammar settled over five rounds of mocks: stone (flat colour, lit band, black joint), cut (the 45° facet on a body stone's outer corner and small on every capsule), inset (the focused pane's gutter stone tucks in from the outer edge, a binder tab; the variant that slid it across to the room was rejected as waste), lens (the one curve, the header stone's foot where the elbow stood, a Fresnel lens round a breathing lamp: distinctive, and an homage), flank (the outer silhouette rounded at its two corners behind a wall, found by accident in a rounded thumbnail), course (bars ending in a point, square junctions, the gallery rail). Built as `data-theme='pharos'` on the existing frame; the one TypeScript change is that the layout now declares the focused pane on the body as `data-focus`, so the frame answers focus without being told. The imported look stays the default, in MEDICAL; PHAROS wears MEDICAL's seven colours in this slice. `data-lcars` renamed `data-theme` since it names a theme and not a franchise, with the remembered choice migrated; face tokens `--face-label`/`--face-data` replace twenty-seven literal font names.
| 2026-09-08 (a component names no theme) | The operator asked whether the listing façade mentions LCARS anywhere. It does not — not its source, its tests, its reference section, its stylesheet block, its changesets or its status rows — and that was deliberate rather than enforced. Now it is enforced: `a-component-names-no-theme` fails CI when the word appears in `features/roster/` or in the component reference. A component speaks its own vocabulary and takes its colour as tokens; AEGIS is where the theme is named, as the origin of the grammar, so that a different theme could sit on the same declaration without a rename.
| 2026-09-08 (PACS declares its levels) | Epic #428, S4, the last conversion. The PACS pane's three levels — patient, study, series — are one declaration into the façade: a child declared through `listingChild_declare` beneath a child, each level with its own traits and tracks, its verbs standing on every row, one filter read down the levels with a parent surviving when a child matches, one sort naming its level. What the pane composed by hand leaves it: three orders, a host, two expansion sets, two render methods, the fold glyphs, the group wrappers, the state line. The façade gained what the conversion needed and nothing it did not: a row that heads a level is wrapped with it in a group that opens and closes as one thing, a level carries its depth so the stylesheet indents by depth rather than by pane, rows can be held open programmatically (a level of one row opens itself), a block set with nothing in it can say so, and a form can ask for a child level's template — which the workspace now carries from the façade rather than from the stylesheet. The four remaining hand-typed track lists are gone; the lint's allowance for this pane, the last, expired with them. Not carried by the façade, deliberately: the summed progress bars and the per-series badges stay the pane's, painted into mounts its own cells register, since a live per-cell channel is not a listing concern. Verified against a live PACS: the query, the fold, the sort inside a study, the provenance readout, the server strip, and the bars still lined up level over level.
| 2026-09-08 (reaching past the façade is the violation) | Epic #428, S5, taken before S4 at the operator's call. `listing-is-one-abstraction` now names the thing it protects: a pane reaching past the `Listing` façade — building a `RosterOrder` or `ListingHost`, calling the row or capsule builders, importing the order or the host, or spelling `--roster-cols` by hand in the stylesheet — fails the gate. The PACS pane is the one allowance, named in the lint and printed as debt on every run, deleted with S4; the four track lists it still declares are listed by selector for the same reason. The dead grid rules the two converted panes left behind (`.files-grid`, `.feedlist-row`'s grid) are gone, and `roster-grid-single-source` reads the façade's `.listing-row` in their place. What the gate cannot yet say is that S4 is done; what it says from here is that nothing else can regress.
| 2026-09-08 (the roster declares its listing) | Epic #428, S3. The runs roster is the second pane to declare its listing into the façade: the order, the host, the chrome lookups, the filter block, the `argus:roster` parse, the action track, the indication model and the readout leave `features/dag/panel.ts`; what remains is a declaration and `rows_set`, with two-line forwarders under the names the surface already calls. The roster takes the expanse rule as it converts: PROGRESS is the `1fr` expanse right after TITLE, which becomes a fixed track, and the trailing facts follow — a running feed's bar is what the eye scans across a roster of seven hundred, and a stub in column five was not that. The façade now refuses a content-sized track at construction (`auto`, `minmax(0, …)`, min/max/fit-content), which is the rule the PACS pane keeps by hand until S4 converts it. The roster's hand-typed `--roster-cols` leaves the stylesheet, and with it the last place a cell count could disagree with the track list — the miscount `roster-totals` was written to catch. Verified: the façade's unit tests, and the roster scenarios of the smoke suite run together.
| 2026-09-08 (the expanse is the middle) | The operator: the PACS progress bar "should be long and span the middle, not packed off to the side", and the bars "must be perfectly lined up". Both had regressed. The series level was designed with its bar beside the description; the patient level and the reshaped study level then put PROGRESS after every fact, at the far right, and each level on a grid of its own — measured, the patient bar began 277 px left of the study's. And the study grid's `minmax(0, 13em)` tracks size to their content, which on a per-row grid means a short name narrows that row alone and every column after it jogs. Settled: at every level the columns read identity, description, PROGRESS as the one `1fr` expanse, then the trailing facts, then the verbs; every other track is a fixed length, never content-sized; the leading tracks are the same widths on every level (the series description widened to meet them), so the expanse begins at one x level over level; and every row and caps row of a listing shares one font size, since the tracks are in em. The listing reference carries the rule; the façade will refuse a content-sized track at construction (S3) and compute the alignment across levels (S4).
| 2026-09-08 (a walk is annunciated on a quiet daemon) | The smoke suite, run whole for the first time since #454, found `index-annunciation` red on a quiet daemon and green whenever an earlier scenario had left the index moving — and the reason was the daemon, not the suite. A feed's topology walk reports its progress into the prompt context page by page, but the calypso heartbeat re-pushed the promptline only when the index counts moved or the previous push still showed warm-up, and a walk adds its instances only once it is done: so for the whole minute the operator most needed the JOBS readout, nothing pushed. Right after boot it worked only because the background roster sweep happened to be moving the counts. The heartbeat now also pushes while a foreground command is executing, which is when a walk can be in flight; `index-annunciation` is deterministic, and `index-movement-is-annunciated` holds on a daemon that has nothing else to say. The same pass repaired the suite's own leaks between scenarios (#480, #483): a scenario starts from a settled console and counts only what it caused.
| 2026-09-07 (the browser declares its listing) | Epic #428, S2. The files browser is the first pane to declare its listing into the `Listing` façade rather than assemble one: the `RosterOrder`, the `ListingHost`, the chrome lookups, the filter block, the strip observer, the `argus:roster` parse, the action track, the indication model, the readout and the selection all leave `features/files/panel.ts` and become a declaration — traits with their own grid tracks, a key, chrome by prefix, row verbs, a selection, a state composer that prepends CWD and STALE. The pane keeps only what a browser has beyond a listing: the card and preview projections (a painter), the cwd binding, the stale readout, and the content views. `--roster-cols` for the files body leaves the stylesheet, computed now from the traits and written on the mount; the node overlay browser, declaring no verbs, stops spending a column on an action track it never fills. Verified by the full smoke suite run together, the browser's own scenarios (row verbs, indicate-not-activate, select as a mode, cards and previews, MKDIR/UPLOAD, the name under the NAME cap) among them. The reference section for the component is amended to describe a thing that runs.
| 2026-09-07 (the listing is declared, not yet on stage) | Epic #428, S1. Three panes assembled the same listing by hand, and after #454 each had added an action track, an indication model, a readout and (in one) a selection by hand too, in two spellings; the grid gained a track and the browser's `..` row kept its old cell count, which no test caught. `features/roster/listing.ts` is the composition owned once: a pane gives a `Listing` its traits (each now carrying its own grid track, an uncapped one for a leading ornament), its key, its chrome by root and prefix, and what a row may do — and the façade computes the template, writes `--roster-cols` on the grid host, blanks the caps over the ornaments, reserves the action track on every row and fills it on indication. Two laws stop being pane behaviour and become consequences of the declaration: declared actions mint the track and split click from double-click, no actions means no track and one click (`a-row-is-indicated-before-it-is-acted-on`); and `rows_set` names the field, so the selection survives a filter and a re-listing of the same name and clears on another (`a-selection-belongs-to-the-field`). Rows arrive in blocks with an optional lead row outside the order (the `..`), a painter may draw a block as cards, a state composer lets a pane prepend its own words or stay silent, and a child level declared through `listingChild_declare` mints its caps per open group on its own grid and reads the one filter down the levels, a parent surviving when a child matches. Proven by the app's first DOM unit tests (jsdom, added for this): every row holds exactly as many cells as the grid has tracks, lead rows and action track included. And it is the first component with a reference: `components.adoc` opens with its section (purpose, anatomy, declaration, states, laws, stylesheet, do-not), under a new law that a component lands with its reference and every slice that changes it amends the section. Not yet on stage: no pane is converted in this slice, and the three live listings are unchanged — S2 converts the browser, S3 the roster, S4 PACS (whose progress summing and form-on-the-study-grid the façade does not yet carry), and S5 makes reaching past the façade the violation `listing-is-one-abstraction` names.
| 2026-09-07 (the proof) | The epic's kernel half has an exemplar of its own, run against a live CUBE: a missing operand asks and the question carries the source's folder and its name, the answer completes the move, abandoning moves nothing and says so, `rm -i` asks and no leaves the file, `rm -I` asks ONCE for three and removes all three, `mv -t` asks for a directory and lands three in it — and a write onto an occupied path is refused by name, with the folder still listing afterwards, which is the property that refusal exists to protect. Proving it at the kernel matters because a surface that loops can make a verb look like it works: `rm a b c` removing three is a different claim from three removals, and only one of them survives a folder of a thousand. 28 checks, twice consecutively, every artefact removed; joined to the nightly list. The surface half is the five smoke scenarios this epic added, which pass together and in either order — one of them found stale: `control-homes` was still descending with a single click, which S3 turned into an indication, and the select-mode scenario pressed the first YES in a scrollback that keeps every question ever asked rather than the one it had just been asked. Screenshots taken for the record then caught what neither the exemplar nor the smoke could: the `..` row was appending one cell fewer than the grid has columns, and a row one cell short does not leave a gap — it shifts every cell of every row after it into the wrong column, so names sat under the glyph track and the verbs ran off the pane. The blanks are counted from the traits now rather than typed out, and the smoke measures what the grid is FOR: every row's name sits under the NAME cap. A picture is a test the tests were not running. Law `a-row-is-indicated-before-it-is-acted-on` (exemplar 13).
| 2026-09-07 (SELECT is a mode, and a selection has verbs) | A mode describes how the field behaves, and this one changes what a click means: while SELECT is on a click gathers a row instead of indicating it, row verbs stand down (a row is not indicated then, it is selected), and the bar reads `SELECT · 2 SELECTED`, adding `· 1 SHOWN` when a filter hides some of what was gathered — a verb acts on the selection, not on what is on screen. The selection belongs to the FIELD: it survives a filter, which is how a selection gets built in a folder of hundreds, and the same rows arriving again (kept by path, which the rows now carry); it is cleared by navigation, since a selection that follows the operator elsewhere is one they can act on without seeing; and leaving the mode clears nothing. Esc leaves SELECT before it retreats anywhere — but never while a question is open, because abandoning a question is an answer and one press must not answer two things. The selection's verbs ride the frame, and each is ONE command the operator could have typed: `rm -rI` over many paths, `mv -t` / `cp -t` over many, `setfacl` over the DISTINCT feeds the selection touches. Three kernel additions carry that: `rm -I` asks once for the whole list and a refusal removes nothing at all, since there is no half of a set; `mv -t <dir>` and `cp -t <dir>` name a TARGET DIRECTORY so every operand is a source, and given no value they ask, wanting a directory; `setfacl` asks once for a set of feeds. The live run found the surface lowering a two-file selection to `mv a b`, which the shell correctly reads as "rename a onto b", and which moved one file of the pair onto the other. That mistake bought something bigger: a WRITE onto a path the store already holds leaves a row CUBE's own API cannot serve, and one such row 500s every listing of that folder — the poisoning first seen in #462, now reproduced deliberately in three commands and traced to its cause. All three routes did it: `mv`'s own PUT, `cp`'s upload of its copy to an occupied name, and — the one that made the original rows — mise's own compensation for CUBE's collision rename, which PUT the wanted path back onto a file whose name was genuinely taken. None of them do now: `mv` and `cp` refuse BY NAME, and an upload keeps the name CUBE gave it and says so. Also here: a browser re-lists after a verb that changed the folder it shows, since `rm` reports what it removed and not where from. Law `a-selection-belongs-to-the-field` (smoke).
| 2026-09-07 (the roster shares) | `setfacl` grants to an identity and applies to a FEED, so the roster is where sharing belongs — a browser row offers SHARE only because the path it holds names a feed. The roster's rows learn the same split the browser's did: a click indicates, a double-click enters, and the action track is reserved on every row so a roster with verbs does not jump. SHARE asks who and says, in the question itself, that a grant cannot be taken back — the irreversibility is stated where the grant is made rather than discovered afterwards, and because the sentence lives in the kernel's ask every surface says it. Beside the verb the row reads back who already holds it, which is a readout and not a verb: it says what the grant would be adding to. DELETE lowers to `feed rm`, which is new — the kernel could delete a feed but no shell verb could — and it asks first by default, naming the feed and what goes with it, with `-f` for a caller that has already asked its own question. Verified live: a feed row carries SHARE and DELETE and reads `SHARED WITH NOBODY`, indicating does not enter, no row moves, and answering no removes nothing. The grant itself needs a second identity, so that check runs when `SMOKE_SHARE_USER` names one and says it was skipped otherwise. Law `a-row-is-indicated-before-it-is-acted-on` (smoke).
| 2026-09-07 (a verb that acts on the place) | MKDIR and UPLOAD act on the PLACE the field holds rather than on any row, so they ride the field's own frame beside HOME and BACK, and they appear on a browser's frame and nowhere else — there is no directory to make in a list of feeds. MKDIR asks for a name and makes it where the field points, not where the session's cwd happens to be: a rooted browser is showing a place of its own, and a verb that acted on the session's place would make the folder somewhere the operator is not looking. UPLOAD is the one verb this surface could not previously speak at all — `upload` reaches the DAEMON's disk, which a browser has never seen — so the operator's own picker chooses the file and the bytes travel over the daemon's `/vfs` route, where the ENGINE writes them through the kernel: no surface talks to CUBE, the write is capped, and the same attach token gates it as gates the read. The console keeps the account, because a gesture the surface performs itself still owes the transcript what it did. Two things the live run added, both about a listing that must be believed: a write through the seam invalidates the listing it changed, as every writing builtin does, and a browser asks for its place again after MKDIR. Law `a-control-lives-where-it-acts` (smoke).
| 2026-09-07 (a row is indicated before it is acted on) | The browser's rows gained their verbs, and both problems that had to be solved first were the operator's own. There was no INDICATE gesture: a click entered a directory, opened a file, opened a `/bin` entry — nothing meant "this one" without also meaning "go". And a row that grew when indicated would make the listing jump, since a capsule is taller than a line of text. So: a click indicates, a double-click activates — the split the DAG scene already teaches — and the action track is reserved on EVERY row at the capsule's own height, declared once and read by the track and the capsules alike, so what changes when a row speaks is what the track HOLDS and never the geometry around it. Only a listing that hides its verbs learns the split: a browser given no verbs (a node's overlay) keeps its single click, because asking for a second click while offering nothing for the first is a worse bargain than the one it replaced. Each verb lowers to a command the operator can read: DELETE to `rm -i` (`-ri` for a directory) so the KERNEL raises the confirmation, MOVE and COPY to a one-operand `mv` and `cp` whose missing destination is the ask that opens the errand. SHARE names its feed — `SHARE FEED 798`, never a bare SHARE, because CUBE grants a feed and never a file — and reads out the grants that already exist beside it; its ask is the kernel's, since `setfacl <feed>` with no entry names what to share and not with whom. A `/bin` row is offered nothing: an executable the catalogue lists is not a file in a store. Law `a-row-is-indicated-before-it-is-acted-on` (smoke).
| 2026-09-07 (the reshuffle) | The browser's drawer held three scopes in one costume: FOLLOW CWD and ROOT HERE, which say what the PANE is bound to; HOME and BACK, which move where the FIELD points; and DOWNLOAD and DELETE, which act on a ROW. Six capsules, one strip, no way to tell from looking which one would act on what. They are sorted now, and nothing new was added while sorting them. Binding joins the binding group beside LINKED FS, because what the next split creates and what this browser follows are the same kind of sentence — and reads as a radio, since a browser either follows the session or holds its place. HOME and BACK move to the mode frame, which is the thing that answers to the field, and they stand down when a diagram holds it. The row verbs leave the drawer entirely; their intents live on as capabilities the console language presses today and a row's own action track presses next. What is left is two groups and a shorter sentence: shape the tile, bind a neighbour. The children group stays for panes whose intents act on the pane as a whole — the DAG's ENTER FEED and REFRESH — and the DAG's own row-scoped verbs are a debt this rule will collect later. Law `a-control-lives-where-it-acts` (smoke).
| 2026-09-07 (a missing operand asks) | `mv foo` names what to move and never says where, and until now that was a usage line — telling an operator the shape of a command they had just typed correctly enough to be understood, instead of asking them the one thing they had not said. A required OPERAND is the same sentence as a value-taking flag given no value, and gets the same answer: the verb asks, with the typed `path` ask that already exists, so a terminal renders it as a line and a surface borrows a browser for it without either verb knowing which one it is talking to. What the ask carries is what makes it answerable: it opens where the source already lives, and its committing control reads `MOVE HERE` / `COPY HERE`. It offers no default name — the only name `mv` could propose is the source's own, and the first live run took Enter as exactly that and moved a file onto itself. The rule that came out of it holds for every path ask: the ANCHOR says where to look, the SUGGESTION says what to offer, and a verb with nothing to offer offers nothing. An abandoned ask moves nothing and says so. Verified live: `mv ~/asktest/real-copy.txt` asks, the answer completes the move, `ls` confirms it, and an empty answer leaves the file where it was. Two defects underneath it were found by this and are NOT fixed here: a source at the home root cannot be copied or moved at all (#462), and a failed `mv` destroys the source (#463) — the second is data loss and outranks the first. Law `an-absent-value-is-a-question`.
| 2026-09-07 (a terminal answers the same question) | The same typed question, on the surface that has one instrument. A terminal cannot borrow a browser, so a location ask renders into its line: the composed default in brackets, and Enter takes the offer — `Where should the table go? [/home/rudolphpienaar/pacs-2026-09-07.csv]`. The answer a terminal commits is then the same answer an errand would have committed, without either surface knowing the other exists, which is the whole point of putting the KIND on the wire rather than a rendering. A yes/no says which letters it takes and offers no default, because there is no safe guess. An older prompt — `hidden` and nothing else — reads exactly as it always did. Closed with a live exemplar (12) that drives the whole path through the kernel with a scripted surface: a flag given no value asks and the question says what it wants, the answer is what happens next, abandoning writes nothing and says so, and a session sitting in a provider path is not offered as a destination. The exemplar caught the last defect of the epic: an abandoned ask was throwing the ANSWER away with the writing. The model crosses either way now — the operator waited for that answer, and only the writing of it did not happen.
| 2026-09-07 (a write is checked, and says what it did) | A chooser can hand a write a place that cannot be used, so `--csv-to` stopped trusting the path it is given. A provider path is refused BY NAME — `/net/pacs/x.csv` is somewhere to browse, not somewhere a file lands, and CUBE's own refusal talks about an upload, which says nothing about why. A missing folder is made and SAID, since a directory appearing without a word is the silent side effect this replaces. An existing table is never overwritten in silence: it takes `--force`, and the pane now states the path the kernel reported writing rather than leaving it to the console alone. Building it turned up three things a mock would never have shown. `files_listAll` answers null both for an empty folder and for one that is not there, so existence can never be inferred from it — doing so reported a folder "made" that already existed and a file "already there" in a folder that did not exist; existence is asked of `files_path_isDirectory` now. CUBE answers a re-upload over a path it already holds with a 500, and that 500 arrives as a THROW: under a daemon an unhandled throw in a builtin takes the process and every surface attached to it, so the write is wrapped and a store's bad day is a refusal rather than an outage. And deletion is asynchronous, so removing a file and writing in the same breath races the store — the removal is waited out, and the write is confirmed by listing afterwards, because a write reported without confirmation was seen to leave nothing behind. Both recorded in `docs/CUBE-gaps.adoc`. EXPORT CSV in argus now lowers to `--csv-to` with no value, so the destination is asked rather than invented.
| 2026-09-07 (a pane on an errand) | A location is asked for by borrowing the instrument that already shows that space. A NEW files pane opens beside the pane that asked — never an existing browser, since hijacking one loses the operator's place and leaves the layout changed after the errand — anchored where the ask said, and it closes when the errand ends either way. Its controls ride a bar of its own across the top: the question as a caption (a readout; it answers no press), the composed path as an editable field, MKDIR for a folder that does not exist yet, and one verb that commits, reading whatever word the kernel sent (`EXPORT HERE`). The grill had put those on the mode frame; building it showed why they cannot live there — the frame is a narrow rail against the spine, right for a column of capsules and hopeless for a caption and a path, and it answers to what the FIELD holds, which an errand does not change. Three things the live run settled, each a defect rather than a preference. The errand opened an empty browser, forever: the daemon runs commands one at a time, so the listing that would answer the question queued behind the command that asked it — the answer waiting on the browsing, the browsing waiting on the answer. An instrument command now runs BESIDE a command that is waiting on a question, which is the natural twin of the rule that an instrument may never ask: a pane's own read neither asks nor waits. The errand also split beside the FOCUSED pane, which can be one the current preset does not hold, and a split beside a pane that is not in the tree fails silently — an errand that never opens is a question asked of nobody, so the host is now a leaf that is actually on stage. And the anchor falls back to home when the session sits inside a provider path: `/bin` is somewhere to browse, not somewhere a file lands, and offering it as a destination is offering one that is refused the moment it is committed. Law `an-ask-borrows-an-instrument` (smoke).
| 2026-09-06 (the surface can be asked) | argus refused every question the session put to it — `the argus surface cannot answer prompts`, with `hiddenInput: false` as the stated reason — so `sudo` dead-ended in the web surface and no control could ask for a value the operator had not already typed. It answers now, in the CONSOLE: where the session speaks, and where the scrollback keeps what was asked, so an operator can look back at the question as well as the answer. A `text` takes an inline line; a `secret` masks the field and enters the transcript as dots — never its text and never its length — which is what lets `hiddenInput: true` be declared honestly; a `confirm` puts YES and NO capsules beside the question, because a control that reads as what it does beats a letter an operator has to know to type, and typing `y` still works for a hand already on the keys. A question outranks the command line while it is open: an answer is never dispatched as a line, and a queued line never jumps ahead of it. Esc abandons, which is an answer of its own — the command is told and reports what it did not do. A `path` is answered here too for now, with its suggestion offered so answering is a rename rather than a whole path typed out; the errand that walks a browser for it is the next slice, and until then a location is answerable rather than refused, which is what matters. Two asks that never travelled were routed through the surface while here: `rm -i` built its own readline against the daemon's stdin, so its confirmation was put to whoever started the daemon rather than to the operator who typed the command, and `upload`'s was untyped. Verified live: `sudo` in argus asks for a username, masks the password, leaks nothing to the transcript, and abandons cleanly. Law `a-question-is-answered-where-the-session-speaks` (smoke).
| 2026-09-06 (a flag given nothing asks) | The kernel's half of the ask. A value-taking flag given no value is a QUESTION, not a no-op: `pacs query … --csv-to` with nothing after it means "ask me where", and the ask carries what it wants (`path`), where browsing starts, a basename to offer, and the word its committing control should read (`EXPORT HERE`). Before this, such a flag was silently ignored — an operator asked for a table and got none, which is worse than either answer. `?` was the obvious spelling and is already a glob in `string_checkHasWildcard`, so it would have expanded against the VFS; the rule needs no sigil at all and now generalises to every flag in the stack. The anchor is the session's own cwd, a fact rather than a guess: inventing a directory means creating one behind the operator's back, which is exactly what the surface used to do. The ask is raised only once there is something to write, since a question about a file that may never exist is a question asked too early. Two rules ride with it, both in the daemon, both about who may interrupt an operator: a command marked `instrument` — a pane's silent refresh, the ambient cycler — is refused an ask outright, because the operator did not issue it and cannot answer for it; and one question at a time per surface, since queuing them invisibly hands somebody a question whose command they have forgotten issuing. An abandoned ask is not a failed query: the answer stands and only the writing does not happen. Verified live on a terminal — the answer completes the write, an empty answer writes nothing and says so.
| 2026-09-06 (a question says what it wants) | The wire has always carried a question — `prompt` / `promptAnswer` — and argus has always refused it: `the argus surface cannot answer prompts`, with `hiddenInput: false` as the reason. So `sudo` dead-ends in the web surface, and the operator's question about capturing an export destination turned out to be a missing primitive rather than a missing field. The first slice is the wire's half: a prompt now says what KIND of value it wants — `text`, `secret`, `confirm`, `path` — so a surface can choose its instrument instead of reading the wording and guessing. A location ask also carries where browsing starts, whether a file or a directory is wanted, and a basename to offer, because an errand that opens nowhere in particular is a browser, not an answer. And it carries the word its committing control should read (`EXPORT HERE`), since a control reads as what it will do next — the RE-QUERY ruling applied to a control that does not exist yet. Everything is optional, so a daemon or surface that predates this still parses and still answers; `hidden: true` with no kind reads as `secret`, which is what such a daemon could only have meant, and that reconciliation is a function in `menu` rather than a rule each surface re-derives — two readings of one message is how a terminal and a browser end up masking different things. An unrecognized kind degrades to `text`, which every surface can answer. No contract bump: the additions are optional, and a bump refuses every older client outright. No surface change yet — this is the sentence the instruments will read.
| 2026-09-06 (an export lands where every surface can reach it) | A PACS answer can be written as a table a spreadsheet reads: `--csv` renders it, `--csv-to <cfs-path>` writes it into ChRIS storage, and argus's EXPORT CSV lowers visibly to the second — the same way GATHER's SAVE already lowers, so the operator reads the command that ran. Two flags rather than one with an optional value, because `--csv PatientID:1234` cannot tell a destination from a query and guessing wrong writes a file named after a patient. The destination exists because a redirect does not travel: a local terminal shares a filesystem with its engine and `--csv > audit.csv` is perfectly good there, but a detached surface does not — the engine runs on the daemon's host, so `>` writes somebody else's disk (#415, where it also turns out to bypass the `engineFilesystem` capability built to prevent exactly that). A cohort's MRNs and study descriptions stay inside ChRIS, and the file browser or `download` retrieves them. The table's SHAPE is the doctrine of the level above, carried through: a row per study, and a row for every patient that owns none — a table built from studies alone would silently drop the misses, which are what an audit is usually asking about. ANSWERED carries an ISO timestamp rather than `3 MONTHS AGO`, because a spreadsheet sorts dates and a phrase is for a human glance. Every cell is quoted and embedded quotes doubled, which is the rule chili's `--csv` has always followed — stated here as a function rather than left inside a handler nobody else can call.
| 2026-09-06 (a server is chosen, never swept) | SERVER became a column of the study listing, and the law placing a form's fields put its control there with no separate decision to make: the cell stands in SERVER's column, wearing the form's own label. It is a state readout you press — `PACSDCM`, `PACSDCM +2` — that unfolds a strip of segments beneath the form, one per registered server, lit when included; the field's touch or Esc retracts it, the same retraction grammar the mode frames use. Not a dropdown: LCARS has no popup layer and should not grow one, since a floating menu is exactly the window chrome this grammar rejects — and a strip of the pane's own width carries thirteen servers where a grid track could not carry three. There is no ALL segment and there will not be one: thirteen registrations of unknown liveness would mean thousands of C-FINDs mostly into the void, so a fan-out is always something the operator named. One server chosen is a CONTEXT and lowers to a visible `pacs connect`, so the session moves and the linked terminal's prompt follows; several is a QUERY, lowering to `--pacsserver a,b` in the editable line and moving nothing. The prompt changing, or not, is the honest tell. The list itself came from the kernel rather than from CUBE directly: `pacs list` now carries a `pacs.servers` model beside the text it always printed, because a surface that asked CUBE itself would be reading a different CUBE from the one its commands run against. Two things the live run settled: the pane's own commands had no way back to it (the DAG pane's already did, so `pacs list` answered into the void until the PACS pane got the same claim rule), and the document's retraction listener closed the strip with the very press that opened it. Law `a-server-is-named-never-swept` (smoke).
| 2026-09-06 (a listing that reports on a set) | The PACS listing gained its outermost level: the MRN, always — not only when a cohort was asked. Every patient asked gets a row, hits and misses alike, because the MRNs that come BACK are by definition the ones with imaging, which makes the answer an operator usually wants — which of these two hundred have none — the invisible half. Three states, distinguishable at a glance: a hit reads its counts, a miss reads `0` with a dim track, and a row nothing could be asked of reads `UNASKED` in the mars hue with its reason on the title, never a zero. The pane's bar carries all three as `FOUND 2 · NONE 1 · UNASKED 0`, derived from the rows so the summary cannot disagree with what is on screen. ANSWERED is a column, since a fan-out replays some rows and troubles the PACS for others: sorting on it lifts the stale rows to the top, and a row with no answer sorts to the far end rather than pretending to be old. Progress sums at this level too — every series under a patient, added and never averaged. A level holding one row opens itself at every level now, so an accession query still costs no gesture while a cohort arrives folded. Two things the live run settled: the FILTER pill reads the OUTERMOST order's strip, which is the one it toggles (it was still reading the study order's and reported OFF while the strip stood open); and the study level is NOT indented, because the form's DATE, ACCESSION and MODALITY fields stand over the study caps and an indent pushed those columns 26px out from under the fields that fill them — the nesting is said by an inset shadow, which costs no horizontal space. Law `a-set-says-what-became-of-every-member` (smoke, needs `SMOKE_PACS_COHORT`).
| 2026-09-06 (a question can be put to several servers) | `--pacsserver a,b` asks each named server and unions the answers, with SERVER carried on every row so they can be told apart, sorted and filtered — the same fan-out the cohort uses, discriminated by a different column. A row names its server only when more than one could have answered: on a single-server query the column would repeat one value down the page and tell an operator nothing. There is no sweep-everything and there will not be one — the CUBE this was designed against carries thirteen registrations of unknown liveness, several of them one-off or per-person, so an ALL would mean thousands of C-FINDs mostly into the void. A fan-out is always something the operator named. The rule settled earlier holds at the surface: one server selected is a CONTEXT and lowers to `pacs connect`, several is a QUERY and moves no context; the prompt changing, or not, is the honest tell. Measured live: a server that does not resolve reads `unasked` with its reason while the server that answered still says what it found — a server that could not be reached has told us nothing about that patient, and rendering it as a zero would say the opposite. The reason is stripped of the error stack's debugging prefix where it becomes data, since it is read in a table and on a graphical surface alike. The SERVER control that stands on this is next.
| 2026-09-06 (a miss is an answer) | The wire could describe what a PACS query FOUND and nothing else, which makes the answer an operator usually wants — which of these two hundred MRNs have no imaging — the invisible half. `pacs.query` now carries a patient level: every MRN asked, its status, its counts, its own query id and its own provenance. Three states, not two: `found`, `none`, and `unasked`, because a query that could not be asked is not a query that found nothing, and letting a timeout render as `0` is the confident stale answer replay was built to refuse. An unrecognized status from a newer daemon degrades to `unasked` for the same reason — it is the only degrade that never reads as an answer. The studies stay where they are, each carrying its own `patientId`; this level records what was ASKED, which is why it cannot be derived from them. Provenance is per patient as well as per answer, since a fan-out replays some rows and troubles the PACS for others. Optional, so an envelope from a daemon that predates the fan-out still parses and reads as the single-question case. No surface change yet — this is the contract the MRN level will stand on.
| 2026-09-06 (a plugin is the one-node case) | `/bin` held two kinds of thing that behaved like two applications: a pipeline was a picture you could open, a plugin was prose you scrolled — the wall of text the operator called exhausting. A plugin is now the graph with one node in it, and there is no third rendering. Its card carries that node, drawn by the shared layout a pipeline's card uses, and it costs no fetch at all: a plugin is one node whatever it declares, where the card used to spend a `cat` per entry to print a paragraph. Its detail is the same stage with the same mode frame, and its node opens as its parameters — flag on the left, what the flag takes on the right, from the `plugin.info` model S4 put on the wire. A pipeline node says the arguments an author already FIXED; a plugin says the ones nobody has fixed yet, and that difference is honest rather than cosmetic. One painter renders both, so the two readouts cannot drift apart. Found live while proving it: opening a /bin entry fetches its graph into a mount, and a listing arriving a moment later replaced the whole view — the fetch then resolved onto a mount no longer in the page and the stage stayed empty (#425). A view is now closed by the operator and never by an arrival; the newer listing lands underneath and CLOSE shows it. Laws `bin-is-browsable` and `a-view-is-closed-by-the-operator` (smoke).
| 2026-09-06 (a plugin is data) | A plugin's substance reached every surface as scraped text: `cat /bin/<entry>` fetched the plugin, formatted a manual, and that manual was the whole of what existed — the operator called the wall of text exhausting, and nothing downstream could act on a paragraph. The kernel now builds a model first and renders the manual from it, so `plugin info pl-dcm2niix-v2.0.0` and `cat /bin/pl-dcm2niix-v2.0.0` say the same facts rather than scraping twice and risking disagreement. `plugin.info` (menu) carries identity, the authoring facts, and every declared parameter with the flag as an operator types it — a plugin's declared `flag` when it has one, `--name` otherwise, because a form built from the name alone would spell `--inputFile` where the plugin wants `-i`. The parameter list is drained to exhaustion: the `getPluginParameters({ limit: 100 })` this replaces was one of the silent truncations catalogued in #401, and a parameter table that stops at a page boundary is worse than no table, because it still looks complete. No surface change yet — this is the wire fact a plugin's one-node graph will read.
| 2026-09-06 (a node opens as what it is) | The operator asked that clicking a node in a pipeline dive in and show `text : detail` pairs, parameter on the left and value on the right. It needed no kernel work at all: `pipelineDiagramNodeSchema` has carried `pluginName`, `pluginVersion` and `arguments` since the model was written, so both the touch and the dive read what already arrived. A touch reads the node out — plugin, version, and how many arguments there are to see; a double-click flies the camera in, as the runs pane's dive already does, and the readout becomes every argument in full. Esc flies back out, one level: out of the node, not out of the view holding it, which is the retreat-exactly-one-level rule the chain has followed since Esc learned about content views. Two small refusals worth naming: a graph at rest says nothing, because a panel of facts nobody asked for is the wall of text this epic exists to remove; and a node with no authored arguments says "none — this node runs on the plugin's defaults" rather than showing an empty panel, since emptiness reads as a failure to load. Law `a-node-opens-as-what-it-is` (smoke).
| 2026-09-05 (the wave, asked for) | The operator wanted a node wave on the `/bin` pipeline view and was asked whether it should breathe or be pressed; they answered "keep the law.. have node wave be user triggered", so PULSE fires one pass on a press and the control reads as the verb. Building it exposed something older: the `/bin` detail had no chrome of its own, but the files pane's mode frame is a SIBLING of the panel's container, so a pipeline's DAG sat on stage while the frame still offered LIST, CARDS, PREVIEW and FILTER — four controls that could not act on a graph. The fix is not a second frame but a truer reading of the one there is: a pane has ONE frame and its blocks answer to what the field currently holds. A listing offers its projection and filter; a diagram offers RANKED/MOLECULE, 3D/2D and PULSE; closing the diagram gives the frame back. The bar annunciates the non-default modes as it does everywhere else. And the wave replays DEPENDENCY order here rather than execution, because a registered pipeline has never run — same animation, different claim, and the pill does not pretend otherwise. Laws `one-frame-answers-the-field` and `a-wave-is-a-verb` (both smoke).
| 2026-09-05 (one layout, two renderers) | The operator opened a pipeline's preview card beside its detail view and said they were "different shape/form", which they were: the card spread each tier evenly while the stage hung a tree of leaf slots with each subtree centred over its span. Nobody chose that. They were written separately — `pipelineSvg_build` in the files panel, `layout_ranked` in the scene — and could not help but differ. Placement is now declared once in `scene/rankedLayout.ts`, dependency-free, and drawn twice: three.js on the stage, SVG in the card, which cannot hold a WebGL context because a grid wants ninety of them and a browser grants about a dozen. What the scene keeps is the third dimension and the metric scaling; what the card keeps is fitting layout units into a viewBox. Being free of three.js, the layout is also the first part of argus a unit test can reach at all — determinism, sibling order, a join taking the deepest of its parents, and a cycle still landing — so argus gained a small jest setup alongside the smoke suite, which needs a live daemon and a real browser. Law `a-preview-is-the-same-drawing` (lint), which fails a renderer that computes its own tiers.
| 2026-09-05 (an answer says where it came from) | A PACS query is the slowest interactive thing in the surface, and CUBE has been storing every answer all along — 1,817 of them on the measured CUBE — so a question asked before need not be asked again. Epic #379 built that, and the operator settled what it looks like: almost nothing. Replay is INVISIBLE. You type a query and results come; when they have been seen before they arrive at once. Nothing exposes the log, the index, or a query id — an earlier draft proposed surfacing the query log as a browsable listing and was rejected, correctly, for making an operator learn a mechanism that exists to serve them. What changes is two things and no more: a provenance READOUT appears in the form's PROGRESS track, immediately left of the button, reading `RESULTS 2026-06-14 · 3 MONTHS AGO` — the date is the fact worth quoting into a note, the elapsed time the judgement worth acting on — and QUERY becomes RE-QUERY in `--october-sunset`, the deeper shade PULL STUDY already wears, never `--mars`, which is refusal. RE-QUERY lowers to `pacs query … --fresh`, which appears in the editable line because everything in this pane is a command the operator can read; when the fresh answer lands the pill clears and the button reverts, so the control always reads as what it will do next. A fresh answer carries no readout: the absence is the statement. The readout was first drawn as a capsule and the operator caught it — a capsule is a thing you press, and one that answers no press is a promise broken, however lawful a filled pill is elsewhere in the grammar (the status strip's ONLINE and HOST lamps are exactly that). The lamps get away with it by living where nothing is pressable; this sits beside RE-QUERY, so it wears the frame's caption type instead of the capsule's costume. Shape is role. Staleness is stated and never decided — an accession names a study that will not change, an MRN can gain one tomorrow, and rather than classify query shapes by heuristic the surface says the age and leaves the judgement where it belongs. Law `provenance-is-annunciated` (smoke, needs `SMOKE_PACS_QUERY`).
| 2026-09-05 (one listing, three panes) | The operator's own reading: "listing objects in the UI are multiple sources of truth when they should be centralized". Files, runs and PACS each spelled the same table — caps, sort, filter, rows, verbs — and a fix to one reached none of the others. Epic #385 pulled them onto one abstraction in four slices; this row closes it with the payoff. The PACS results are now a framed field like the other two: the frame outside the scroll, a mode frame on the spine carrying FILTER, `pacs sort` and `pacs filter` in the language. Its columns are declared once as traits, its PULL and GATHER are actions outside the column grid (a verb answers to no cap), and its levels FOLD — a study keeps its place and opens its series inside it, where a directory or a feed REPLACES the level above. Both levels are listings: the operator's first look at this said "I didn't see any col headers", and they were right twice over — a study was a run-on label rather than a row, and an accession, the number a report is filed under and the one an operator arrives holding, was buried in the middle of it. The STUDY caps now head the region before any answer arrives (PATIENT, MRN, STUDY, DATE, ACCESSION, MODALITY, SERIES, PROGRESS), and a study's SERIES caps are minted inside it when it unfolds. The filter reads down the levels: a study matching on its own columns keeps all its series, a study that does not keeps the series that match, and a study with neither leaves the stage — so an accession gives a whole study and a modality gives the series. One order state governs every study on stage, so a sort touched in one lights and orders them all: `RosterOrder` learned to mint a caps row per group. Progress now reads at every level, summed and never averaged — a study's bar is its series' addition, so one finished series of ten files cannot outweigh a stalled one of ten thousand — and a series nothing has been asked of carries a dim track reading NOT RETRIEVED, because an absent bar reads as "no such thing" where a dim one reads as "nothing has happened yet". Also found live: the same answer can arrive twice, and the second arrival was throwing away the fold the operator had just opened — a repeat of a query now keeps the folds and the retrieves in flight, and only a new query resets the pane. The operator then asked whether the search bar should follow the same order and layout, and it should: the form now stands on the STUDY grid in the STUDY caps' order (PATIENT, MRN, —, DATE, ACCESSION, MODALITY), QUERY sits where PULL STUDY sits, and the editable `pacs query ...` line it generates reads left to right as the listing does. STUDY gets a gap rather than a field: `StudyDescription` reaches the PACS and the PACS ignores it — measured, both studies came back including the one that does not match — so it is an optional C-FIND key a server may drop, recorded in `docs/CUBE-gaps.adoc` and offered as a column but never as a control. Law `a-form-stands-where-its-answer-will` (smoke). The remaining level is the patient: an MRN-level listing folding down to studies, which needs the query model to carry patients (epic #379). Caught while verifying live: `pacs sort` and `pacs filter` joined the language, and `pacs` is also a kernel command — the surface swallowed `pacs query` and answered "unknown verb". A shared subject now claims only the verbs the session lacks and falls through for the rest, under law `surface-never-shadows-the-session` (lint), which reads the kernel's own dispatch table so the next collision fails CI instead of an operator's query. Laws `listing-is-one-abstraction` (lint) and `progress-sums-its-levels` (smoke, needs `SMOKE_PACS_QUERY`).
| 2026-09-04 (warm-up leaves the gate) | The boot readout blocked on four prefetches in front of the prompt. Measured against a live CUBE: `/PUBLIC` costs 8.4s and `/SHARED` 9.3s — eighteen seconds an operator spent watching a prompt that was already theirs, buying freshness the stale-serve path delivers a moment later anyway. Now boot blocks only on `/bin`, which completion cannot work without; Groups, Feeds, Public and a newly added `/SHARED` step warm behind the prompt under a new `[PENDING]` boot status. `/SHARED` had no step at all before — nothing to fail, so a CUBE that stopped serving shared paths stayed silent until somebody went looking. A deferred step leaves the boot failure gate, so its failure can no longer stop a daemon binding; it is instead held in the kernel until a later attempt succeeds and carried on the prompt context as `warmupFailures`, said by chell's prompt (`[warm-up failed: groups]`) and named on argus's JOBS readout in mars. Named, not counted: "Groups" tells an operator which capability is degraded where "1 warm-up failed" only tells them to go looking. Law `deferred-warmup-failure-persists` (smoke). Doctrine home: this row and the boot-cache epic #367.
| 2026-09-03 (index movement) | The operator asked what signal says a newly shared 20k-node feed is being indexed, and when it is done, and whether that belongs in a header pane. Traced: the global sweep already reached the prompt and the JOBS readout, but a feed's first-visit load — the 20k case — emitted nothing, and a feed the roster gained (created, or shared to this identity) arrived silently. Now the kernel keeps two more registers on the process cache (one feed's load in flight; roster arrivals for thirty seconds) and the prompt context carries them on the same `procWarmup` segment (`feed`, `arrived`, and `sweeping` so a renderer can tell a sweep from a load). Chell's prompt says `feed 812 indexing: 3400/20000 17%` and `+feed 812`; argus's JOBS readout says `FEED 812 3400/20000` and `+FEED 812`; a roster on stage pulses the arrival's row and re-lists when it does not yet hold the feed. The daemon's heartbeat keeps pushing the promptline while the segment is present, so the count climbs with no command typed. Law `index-movement-is-annunciated` (smoke, needs `SMOKE_BIG_FEED`).
| 2026-09-03 (the runaway canvas) | The operator reported the /bin pipeline view "regressed": text, no diagram, no CLOSE, a horizontal scrollbar. Not reproducible headless — and that was the lesson: headless Chrome runs at device pixel ratio 1, a desktop at 1.25 to 2. The scene sizes its canvas in DEVICE pixels and never writes the CSS size; the DAG pane and the header cycler each pinned the canvas with a stylesheet rule of their own, the /bin diagram mount had none, so at any ratio above 1 the canvas's device width fed back into its mount's width on every resize until it was 33 million pixels wide, pushing CLOSE and the graph out of view. Fixed at the source: the scene tags its canvas (`.dag-scene-canvas`) and one rule pins every mount. The smoke driver learned `SMOKE_DPR` (CDP device-metrics emulation) and the suite runs green at 1.5×; the bin-context scenario asserts no sideways overflow. Law `canvas-fills-its-mount`.
| 2026-09-03 (host control) | Grilled with the operator, eight decisions: the daemon may declare capabilities of its own, the AI-harness permission model — `chell --daemon --host-control[=shell,files,pipes]`, off by default, host always wins under it, refused on a network bind without `--expose-host-control`, `sh -c` with stdin closed and output on the bus, `upload` refuses without the `files` tier, surfaces learn it from the attach ack. Argus's part: the status strip's amber HOST lamp (annunciation, never at rest), read from the ack's `hostControl`, and the refusal text for `!` on a daemon without the policy now names the flag. The `edit` relay is a different thing — argus needs an editor of its own (#356, CodeMirror). Law `host-control-is-annunciated` (smoke). Doctrine home: `docs/session-supervisor.adoc`, "Host control".
| 2026-09-03 (hue is a mode) | The operator asked whether pipeline glimpses should rotate and whether nodes should be colored by compute assignment. Rotation: no — the dark cockpit animates nothing at rest, ninety spinning thumbnails is noise, and the header's ambient cycler is the one deliberate exception. Compute: yes, where the data exists. CUBE fixes a job's compute at run time, on the plugin instance (`compute_resource_name`), not on the pipeline's pipings — so a registered pipeline's glimpse has no compute to show and stays monochrome, while a FEED does. The process cache now records each instance's resource (cumin + salsa, from the list row it already reads), the `feed.dag` node carries it (`mixed` for a group whose members disagree), and the DAG pane's mode frame gained a HUE block: STATUS (default) or COMPUTE, which colors nodes from the frame's own hue cycle in order of first appearance, errors still winning, groups that ran on several resources in grey — and the legend travels with the mode as swatches on the bar readout (`COMPUTE ▪host ▪galena`). The block dims when no node of the feed reports a resource. `dag hue status\|compute`; a desktop saves it. Law `hue-is-a-mode` (smoke).
| 2026-09-03 (totals, targets, glimpses) | Three from the operator. The feed roster now carries SIZE and TIME — total output bytes and the wall span from first node start to last node end (to now while running) — derived by the kernel from resident nodes with no wire (`feedTotals_derive`; an 88k-node feed is 88k additions), and only where they mean something: the RUNS roster, never a CFS listing such as `~/feeds/feed_N`, which is a directory and not a run. A feed whose topology is not resident reads a dash, never a zero. A link now says where it points (`→ target` beside its name in LIST, its own line in CARDS), and entering one works: `cd` on a CFS path ending in a link had gone to the real-folder validator, which asked CUBE for a folder at the link's own path; it now follows the link's target, and a refusal names that target. And PREVIEW in /bin: a plugin's glimpse is the head of its description (through the session's `cat`, since /bin has no bytes to serve), a pipeline's is its authored graph drawn small — tiers by depth, no physics, an SVG that scales to the card, and a node count instead past eighty.
| 2026-09-03 (nits, and why there were so many) | The operator asked why the frame kept shipping with nits. Honest answer: each pass was verified by geometry and by crops of the corner it changed, never by looking at the whole composition against the canon — so a hover that flipped to near-white, three frame members in one flat hue, a focus ring cut by sticky children, a lamp whose black type lost to the strip's readout rule, and two roster grids spelled twice all passed. Fixed together: a hover BRIGHTENS a control in its own hue and never glares (law `hover-never-glares`, lint); every segmented frame member — caps, mode blocks, the bar's fill — cycles the frame's three hues (harvestgold, daybreak, orange), as the gutter does, so columns and modes read apart without gaps; caps are bar segments with a uniform 3px of black whatever their grid gap, so the files and runs rosters look alike; the roster grid is declared ONCE per host (`--roster-cols`, `--roster-gap`, `--roster-pad`) and read by caps and rows both (law `roster-grid-single-source`, lint); the focus ring rides a veil above sticky caps and headers; the lamp's rule outranks the strip's; and a node's extensionless files (log, params, status) preview as text until their head proves binary.
| 2026-09-03 (the bar) | Second look from the operator, three screens: the blocks beside the spine still read as floating buttons of a new kind, not frame; the node's browser inside the DAG had no frame at all (unscoped caps wrapping OWNER, the canvas elbow peeking past it); a hard-coded orange focus ring in sickbay; NAME jammed against its cap's curve; a native scrollbar; ONLINE in green-on-green. All fixed: the spine now WIDENS into the elbow's vertical bar when touched, and the blocks are that bar's SEGMENTS — labels on the bar, black gaps between, a plain fill to the foot — so the open frame is unmistakably frame, costs field width (the incentive to dismiss it), and narrows back to the spine on touch, fill, field, or Esc. The node browser is stamped from the files template (same frame, same caps grid, scoped to any files body), and the canvas frame steps aside while it is up. Every translucent frame color derives from the theme through `color-mix` (rings, glows, dim blocks), the caps' ends carry padding matching their radius, scrollbars are thin frame-colored bars on black everywhere, and the lamp wears black type.
| 2026-09-03 (the frame, drawn) | The operator looked at the first mode frame and saw no frame: a black gradient on black with floating pills, and an elbow stub joining nothing. Not LCARS — the frame is solid. Settled with the operator: every field with modes is framed at rest by a RULE along its top (the files pane: the path header's rule, thickened, the header made sticky under the caps so the rule never scrolls away; the DAG canvas and the feed roster: a rule under the bar) turning through a real ELBOW (fat, inner radius cut in black) into the SPINE down the right edge. The join is static — nothing has to line up during a slide. Touch the spine and the MODE BLOCKS slide in under the rule, flush against the spine, stacked with the sidebar's thin black gaps — the column is the frame, no panel. The scroller sits left of the spine. And the filter: doctrine said a mode summoned on demand, the code hid it by default, and the CSS (`display: flex` beating `hidden`) had it standing on every roster since the caps landed — the smoke had read the attribute, not the geometry. FILTER is now a block on the mode frame (files and runs), reading its state; the drawer's FILTER capsule retired (one control per mode; `file filter` / `runs filter` lower to it); the strip folds out under the rule and the smoke measures it folded at rest and open after the block. Law rows unchanged; the caps-are-the-sort statement now names the block.
| 2026-09-03 (the mode frame) | The operator caught the LIST / CARDS pill sitting in the files pane's field, lower right, and asked why the gate had not — and the honest answer was that doctrine had legislated it: the 2026-08-31 rail rows moved the DAG's mode pills "off the header bar into an in-pane rail", and the files pill copied that. The rail's TAXONOMY was right (display-content modes, apart from the drawer's machinery); its PLACEMENT was a web-map control in orange — nothing floats over the field in Okuda's grammar, controls live on the frame. A frame column at rest failed the arithmetic (a capsule column is ~100 px; a pane with one pill would give up 95 % of it to empty frame), the drawer alone would hide state and cost a gesture per flip. Settled with the operator by riffing on a law already on the books: the header gets out of the way and leaves a strip; a pane's MODE FRAME does the same at pane scale. A thin strip at the field's right edge at rest; touch it and the frame slides in on its elbow with the mode capsules; touch the field (or Esc) and it retracts. Open on touch, never on hover (hover only lights the strip: every affordance by touch, and hover-open would collide with orbit and drag); it overlays the field as a transient (never reflowing the 3D buffer) and joins the Esc chain beside the drawers; the same edge on every pane that has modes, and no strip on a pane that has none; the bar annunciates every non-default mode (`MOLECULE · 2D · CENSUS`, `CARDS`) so state stays legible with everything retracted, and nothing at rest. The language is untouched — `dag projection 3d` lowers to the same capsule. Laws `field-is-content` (lint: every mode pill inside a mode frame, no control in the DAG or files field, and the repealed rail's CSS deleted — a repealed rule left standing enforces the repeal's opposite) and `frame-retracts-to-a-strip` (smoke). The 2026-08-31 rail placement is repealed; its taxonomy stands.
| 2026-09-03 (PREVIEW, corrected) | The 2026-09-02 cards row read PREVIEW as listing-plus-selected-file and held it as a split verb. Wrong: the operator meant the file manager's icon view — every entry drawn with a rendered glimpse of its content. Built as the third projection on the files mode frame (LIST / CARDS / PREVIEW, `file preview`): a card that leads with a thumbnail — an image served natively from the token-gated `/vfs` route, the head of a text file (a bounded read of the same route, the body cancelled once 600 bytes are in — a preview never pulls a whole log), or the kind's glyph when nothing renders (a directory, a plugin, a DICOM file, a text file over 256 KB). Fetched only when the card scrolls into view (IntersectionObserver over the panel), heads remembered per path. The slaved VIEWER remains what it was: a split with a viewer binding.
| 2026-09-02 (cards) | The operator asked for a card view of the Files pane, pointing at the prototype's `AssetCard`. Built as a projection mode on a files rail (LIST / CARDS, the pill reading the current mode like the DAG's): the same listing, the same caps sort and filter strip, the same activation (a plugin card opens its description, a directory card enters). Cards carry the kind as a colored badge, the name, owner and date, size. A preview mode was NOT added as a third projection: listing-plus-selected-file is the slaved VIEWER that a split already gives, so PREVIEW will be a verb that performs that split (held). Law `projection-is-a-mode`; `file list\|cards`. _Superseded 2026-09-03: PREVIEW is the thumbnail projection, and the rail is the mode frame — see the two rows above._
| 2026-09-02 (the language, written down) | The operator asked for the visual action space as a document. `docs/argus-lang.adoc` is the long form of `argus verbs`: grammar (`subject [@id\|%n] verb args`), every subject and verb with what it lowers to (drawer capsule, rail pill, gutter given, DOM event, or session command), the desktop script format, and the completion vocabulary. Rule stated there: a verb, its help line, the doc, and the ledger move in one PR. Found while writing: `argus verbs` omitted `runs sort\|filter`, `file sort\|filter`, and `node clear`; fixed.
| 2026-09-02 (a follow answers a move, not a promptline) | Operator report, the real one: feed 2583 opened and a few seconds later the pane showed feed 21. Cause: the session cwd was parked inside feed 21 (left there by a test on the shared daemon session), and the cwd-follow treated every promptline whose cwd feed differed from the pinned one as "a different feed underfoot", replacing the pick as soon as the pane's own silent command pushed a promptline. The follow now acts only when the cwd actually changed since it was last observed: a pin survives until the operator cds elsewhere. The smoke restores the session cwd after moving it.
| 2026-09-02 (a turned tree is still the tree) | Operator report: dragging the background of BLT-TEST-3 "jumped to a different feed". Reproduced headless: same feed, same title — an empty-space drag orbits, and the ranked tree carried a per-node depth jitter (a parallax gimmick from the tier-spread days) that, seen from the side, scrambled the hierarchy into an unrecognisable graph. RANKED is now a sheet, every node at z = 0: an orbit turns the sheet and the tree stays the tree. Orbit in 3D and pan in 2D are unchanged.
| 2026-09-02 (a files row says what it is) | Operator catch: the Files pane had no kind column — a name alone does not say plugin, pipeline, link, directory, or job. TYPE joins the grid between NAME and SIZE, sortable like every column, caps and rows on the same template.
| 2026-09-02 (Esc pops a content view anywhere) | Operator catch: Esc did nothing over a /bin entry view. The contextual-back chain knew node overlays and DAG levels but not a files pane's own content view. Now a files pane's content (a file, a /bin entry) is a level in the chain — the focused pane answers first, else whichever pane has content up — so Esc returns it to its listing exactly as CLOSE does.
| 2026-09-02 (/bin rows open as context) | Operator ask: browsing /bin, a plugin or pipeline row did nothing. Now a plugin row opens `cat` of the entry — the description grammar highlighted (headings, `Key:` labels, flags, quoted values; ANSI from the daemon rendered as sent) — and a pipeline row opens its summary with the `pipeline diagram` model rendered in a DagScene beneath it (flex-filled work surface per the desktop contract), disposed on CLOSE. Same content view, same Esc level, as a file. Law `bin-is-browsable`.
| 2026-09-02 (the console's browser, declared) | The operator rediscovered a binding they had designed and forgotten: the primary browser follows the session cwd. Correct default (terminal plus file manager), wrong legibility — same chrome as a rooted split-born browser, no readout, no verb. Settled: the bar's state reads `CWD` while following; FOLLOW CWD / ROOT HERE in every files drawer (idempotent, both offered — a control reads as what it does); `file follow` / `file root`; console listings reach every following browser; a browser that starts following lists the cwd at once; FILES-01's hint names it the console's browser. The roster pick stays view-local (2026-08-31 stands); ENTER FEED is the session move. Law `following-is-declared`.
| 2026-09-02 (Esc pops the file view; ranked is a tree) | Two operator nits. (1) Inside a node, Esc on an open log left the whole node — contextual-back applied: a file view is a visible level, so the first Esc returns to the node's listing and the next leaves the node; directory depth stays BACK's (invisible depth is never walked by Esc). (2) RANKED spread each tier's nodes evenly and sorted by id, so children landed nowhere near their parents and edges zig-zagged across the pane; the old ChRIS UI's tree read at a glance. RANKED is now drawn as the anchor tree: every node beneath its first present parent, a subtree over a contiguous span of leaf slots with its root centred on the span, joins drawn as extra edges without moving anything. Same determinism, relationships read straight down.
| 2026-09-02 (caps are a frame, and frames stay put) | Operator review of the roster caps: too thin, labels right-aligned over left-aligned cells (read as misaligned), a real user's name in the filter hint, and the question of whether caps scroll with the rows. Settled: caps are the roster's frame — taller (24px), each label aligned exactly as its column's text (ID and the numeric file columns right, the rest left, no padding the cells lack), anchored (`position: sticky`) with the filter strip while rows scroll beneath, measured cap-to-column geometry exact on both panes. Hint text names no one. Also caught: a fresh page whose cwd sat inside a feed had RUNS-02 land on the roster and then repaint the cwd's feed over it — the cwd-follow now yields while the roster (or a selection in flight) is on stage.
| 2026-09-02 (ENTER lands in a place) | Operator catch: ENTER NODE from the roster ran `cd` on the group's last regard, which a file click inside a node had left pointing at `.../log` — "Not a directory". Two faults: the verb trusted the cell's shape, and a roster pick wrote no regard at all, so there was nothing honest to enter. Now ENTER NODE resolves a place (a file regard becomes its directory, a node regard its data, none the feed on stage), the roster pick writes the feed's address as regard (`modelKind: feed`), and ENTER FEED sits beside ENTER NODE for the session-global move into the feed. Law `enter-lands-in-a-place`. Also recorded: a roster pick still stays view-local by the 2026-08-31 decision — it does not move cwd; ENTER FEED is that move.
| 2026-09-02 (a selection answers at once) | Operator catch on the 88k feed: selecting it left the roster on stage and hoverable for the seconds the projection took — the pane looked idle while working. Honest-wait applied to selection: the roster steps aside at the click, the pane reads RETRIEVING FEED N…, the bar reads LOADING (butter, no animation — one animation author is CSS and this needs none), Esc backs out of a selection in flight, and a refusal returns the roster with the reason above it. Law `select-answers-at-once`, smoke-enforced.
| 2026-09-02 (listings survive a restart, honestly) | The fifth PR of the live-/proc campaign: directory listings are checkpointed (identity-keyed, throttled) and restored with their original timestamps, so a restored listing is served STALE and revalidated behind itself — the Files pane's bar says STALE until the session's refresh arrives on the ambient bus and swaps the listing in place. Found and fixed on the way: the listing path served any cached entry regardless of its TTL, so a listing never refreshed until eviction or `ls -f`; now a fresh entry serves, a stale one serves-and-revalidates where a host can carry the refresh (the daemon), and refetches in line at a plain console.
| 2026-09-02 (the pane is the subscription) | Grilled with the operator: a dynamic pipeline grows on CUBE while the DAG pane watches two nodes forever — the old UI's reload problem in a new place. Settled the kernel half in three PRs: `/proc` freshness became VISIT-DRIVEN (a revisit is a delta — feed counters, then only the nodes created, finished, or still running since the cursor; settled feeds re-checked per ten minutes; the roster picks up newer feeds on every `/proc/jobs` visit and walks whole once it is ten minutes old); the checkpoint became per-feed SHARDS with a per-shard write floor (a growing feed rewrites only itself); and liveness got its own noun — a WATCH (`watch`/`unwatch` wire pair, owned per surface, released on detach; `proc watch|unwatch` at the console) drives an adaptive sampler (3 s while changing, 30 s when quiet, stops on settle) whose refreshed `feed.dag` models ride the session bus from the `daemon` surface, with `watched` reporting live / settled / stale. Regard stays what it is: what the operator indicates, not what a surface keeps open. Surface half built in the fourth PR: the DAG pane opens its watch on entering a feed and releases it on leaving (Esc, RUNS-02, close); daemon-originated models refresh the pane in place (same groups and counts patch status; anything else re-lays out warm, so the census re-expands only when a count moved) and never reach the transcript or pin; the bar reads LIVE / SETTLED / STALE; REFRESH is a drawer capsule (`dag refresh`); the roster re-asks the session once a minute while it stays on screen. Law `pane-is-the-subscription`, smoke-enforced.
| 2026-09-02 (caps must sit over their columns) | First use returned two catches. The caps were an equal-width flex strip over CSS-grid rows — they could never line up; they are now the SAME grid as the rows they head (same template, same font-size since em columns resolve against the grid's own font, same row padding), with the DATE/CREATED columns actually shown so every cap has a column. And each keystroke's re-render re-inserted the strip element, blurring the input after every character (and the repeated full re-render was the road to the crash): the caps + strip now stay mounted across re-renders, which only replace the rows after them, coalesced to one per frame. Verified: cap lefts equal cell lefts within 2px in both panes; a 28-character filter typed without losing focus once.
| 2026-09-02 (order and filter, one grid) | The operator found both rosters missing per-column order and a filter. Grilled to one answer for both: a shared RosterOrder over the resident rows (no wire). Column caps ARE the sort control — the table's own frame, not a title bar, so the bar law is untouched: touch to sort, again to reverse, active cap lit with ▲/▼ (FILES: NAME·SIZE·DATE·OWNER, the `ls --sort` vocabulary; RUNS: ID·TITLE·STATUS·OWNER·CREATED). Filtering is a mode: the drawer's FILTER capsule (or `file filter …` / `runs filter …`) summons a strip — substring across columns, `owner:sandip status:error` for scope — and the title bar carries the STATE, `FILTERED 12/695` (state on a bar is lawful; closing the strip clears the filter so hidden state cannot lie). Sort by sentence too: `runs sort created desc`. New law caps-are-the-sort with smoke assertions.
| 2026-09-02 (every pill a question, every knob a sentence) | Grilled: should the physics terms be rail pills? No — the rail carries MEANINGS (which layout, which metric, shape or census), and charge/link/collision are the implementation of an honest layout; a pill to switch collision off is a pill to request interpenetrating lies. Expert knobs belong to the language: `dag physics charge|link|collide|gravity on|off` and `dag physics reset` reach the pane through DOM events (verbs run over the DOM, never a private API). Mass is the exception — "heaviest at the heart" is a meaning an operator wants or not — so mass-weighted centering earned the GRAVITY ON/OFF pill (rail convention: label = current state, dim when off). And the node detail got its off switches: a click on empty space clears it (the natural gesture), the DAG drawer offers CLEAR DETAIL, and `node clear` says it — the operator could not find the selected member again in an 88k cloud to click it off.
| 2026-09-02 (radius is physics, not paint) | Operator insight, upheld: the molecule sim was metric-blind — uniform charge and link distance, the radius painted on afterwards — so a 43-hour stage and a 4-second one claimed identical space. Now the metric IS the physics: repulsion scales with cross-section (r²), links reach surface to surface (rₐ+rᵦ+base), and a collision force at 1.2r forbids interpenetration. TIME and SIZE therefore reshape the molecule, not just its spheres — verified on 2583: TIME a downward cascade with the core held high, SIZE a starburst around a central core. Re-projections warm-start from the standing positions (fewer ticks), so a metric flip morphs the graph instead of re-rolling a new equilibrium. Census inherits it: shell radii follow the anchor radius. Mass-weighted centering (heaviest stage at the heart) deliberately deferred until the operator judges the three terms.
| 2026-09-02 (census is a place, not a diorama) | The operator flew the census and wanted to live there: enter nodes, fly through the core, feel the pulse. The "draws, never picks" rule was my convenience, not law — repealed. Census now raycasts the InstancedMesh (instance → group id), so a member selects (it lights white; the facts chip shows its group's ×N, FAULTS, WALL, SIZE) and double-clicks into its group's representative (the flight targets the member's position). The wheel in census FLIES along the eye ray — untethered from the origin, no floor, through the core and out — while shape keeps its orbit dolly. And the pulse rides the cloud: every member flares with its group's fire time, lerped in place on the instance color buffer (88k members, one buffer update per frame). All verified live on feed 2583.
| 2026-09-02 (the dive: three catches) | First flight of the census returned three operator catches. (1) Zoom inside a SPLIT left the leaf imprisoned in its old cell — siblings hid, but no ancestor box yielded; every split on the path above the zoomed leaf is now marked (`pane-zoomed-path`) and conquers its full region (smoke: split-zoom). (2) The wheel dollied along the z axis with a 3..40 clamp — fine on-axis for shape, but census parks the camera off-axis and far, so a dolly slid the cloud diagonally off screen; it now dollies along the eye ray with a far-aware clamp (verified: wheeling into the 88k organism keeps the core centered). (3) "Can't double-click a node" was not the picker — with the console open the DAG canvas was 353px tall and the collapsed chain rendered as a sub-pixel sliver; with zoom truly full-viewport a node lands on first sweep and dblclick opens the overlay (pl-dyanon, WALL 43h). Open thread: shape-view camera framing for a squashed canvas is still too timid. Also noted: under RANKED anchors the census reads as a spiral galaxy (planar anchors + fibonacci winding); MOLECULE gives the organism — both honest.
| 2026-09-02 (census as spectacle) | The operator asked for the fsv moment and got it: the SHAPE/CENSUS rail pill expands every ×N group client-side into instanced members on analytic fibonacci shells (the force equilibrium of a symmetric fan is known in closed form — place, don't simulate), with equal-count parent/child groups index-paired so chains of groups render as branched filaments. Feed 2583 draws all 88,578 members in ~12s, one InstancedMesh, fully interactive; under molecule anchors it reads as a living organism. CENSUS draws, never picks — selection and facts belong to SHAPE. Derived entirely from the tally counts: zero extra wire. The same session also proved the repaired checkpoint (695/95,591 restored to `current` in seconds) and the warm path: 2583's shape projection in 4.8s.
| 2026-09-02 (scale renders as shape, not census) | The 88,578-node shared feed (feed 2583, 93% of a 95,591-instance index) forced the settled DAG design's ×N aggregation into the wire itself: `feed.dag` now ships the COLLAPSED projection (the terminal tree's own isomorphic-sibling transform), one ×N node per group with a real representative instance, worst-member status (error-visibility is not negotiable), summed metrics, and a jump-to-error tally the facts chip renders (COUNT and FAULTS rows). The scene needed no new machinery — labels, scaling, node-hop, and regard all work on representatives. Unit-tested; live characterization against 2583 recorded alongside.
| 2026-09-01 (focus citizenship) | Debated with the operator and settled against second-press-opens-controls (press-count as hidden state; machinery pulled away from the frame; would gut the day-old gutter law). The real inconsistency was FOCUS: the prefix key's "focused pane" could never be the console — the arbitrary distinction the operator smelled. Now the console is a focus citizen: the operator's last touch (click or focus, console area in / workspace tree out) decides who owns Ctrl-B, and every given focuses the channel it renders. The composition falls out derived, not special-cased: any given, then the prefix, is that channel's controls. Law row focus-citizenship + three smoke assertions (29 green).
| 2026-09-01 (the console is summoned like anything else) | Operator verdict, upheld: the lid as the console's ONLY door was undiscoverable and a language violation — "special" is an implementation fact, not a user-facing distinction. CONSOLE-05 joins the gutter as a live given (gutter law: always renders the console open with the prompt live, never toggles — smoke-asserted); the lid remains the tactile shortcut. COMPUTE renumbers to -06. And the grilled ordinal question became law: a `-0N` ordinal marks a SUMMONABLE DOMAIN CHANNEL, numbered sequentially down the column; meta givens (LANG — the language, not a channel) carry none, the bareness itself information. New lint check ordinals-mark-channels enforces numbering and LANG's bareness (negative-tested: a mis-numbered channel fails CI).
| 2026-09-01 (the console joins the grammar) | The console's zoom lost its touch path when the bar law executed the old zoom block riding its title bar — rightly killed, never rehoused. Settled with the operator: THE CONSOLE IS A PANE THAT DOES NOT SPLIT. Its header is now a handle like every pane bar; its drawer carries only the verbs that apply — ZOOM (the shared data-pane path, so RESTORE relabeling is free) and CLOSE (retracts through the lid's own toggle). No SPLIT: one session, one voice — and no binding, since binding governs what a split creates; the absent groups are the grammar telling the truth. New law row console-joins-the-grammar with four smoke assertions (25 green). The gate earned its keep mid-build: it flagged its own bar extractor swallowing the new drawer's buttons — fixed to parse through the end-cap.
| 2026-09-01 (a repealed law must be deleted) | Operator caught RESTORE missing from the zoomed drawer: the ORIGINAL grammar spec ("zoom only when unzoomed, Esc is the way back") still had its display:none in the CSS — superseded by control-reads-next-action but never repealed, so the relabeled capsule was correct and invisible. And the smoke assertion had passed on textContent, which reads hidden elements: a control must be ON STAGE to count, so the scenario now measures geometry. Lesson beside the two-authors row: when a law changes, hunt down and delete the old law's enforcement — a repealed rule left standing enforces the repeal's opposite.
| 2026-09-01 (doctrine grows teeth) | The operator's question — why do regressions of WRITTEN law keep landing — had one honest answer: the ledger was prose and nothing mechanical read it. AEGIS now opens with "The laws (machine-checked)": twelve laws, each naming its enforcement — five static checks in scripts/aegis-lint.mjs (drawer-everywhere, bar-no-machinery, caps-are-punctuation, lid-is-mute, single-animation-author) gating every pull request in CI, seven scenario references into the smoke suite. The gate also enforces the table itself: a law naming a nonexistent check or scenario fails CI, so doctrine cannot be written without teeth again. Proven against history — re-introducing the CONSOLE lid label fails the gate with lid-is-mute.
| 2026-09-01 (zoom is immersive; a given never toggles) | Three operator catches in one report. PACS-03 toggled between PACS and home — a GIVEN ALWAYS RENDERS ITS TARGET (gutter law, now explicit): the toggle died, dismissal is the drawer's CLOSE. Zoom carried too much frame for an immersive intent: the lid, the status readouts, and the stray corner gradient now step offstage with the header, leaving only the thin restore strip — functional, as it always was for header-away: touch it (or Esc) and the composition returns. And the drawer's ZOOM capsule now reads as its next action — RESTORE while its pane is zoomed (the missing-control report; RESTORE over "unzoom", matching the strip's own verb). All three joined the smoke suite (gutter-idempotency; framing, strip, and capsule assertions) — 21 green.
| 2026-09-01 (invariants become a suite) | Two more operator catches — zoom left a crushed header band on stage (offsetHeight missed the first bar's collapsed top margin; the slide now travels the header's measured viewport bottom) and PACS-03 had NO pane controls (its workspace was legacy markup: no handle, no drawer, and a red machinery cap on its bar — it now carries the standard drawer grammar, a passive cap, and chrome wiring like every pane). The pattern (every violation was of a LAW this ledger already states) demanded teeth: tests/smoke/smoke.mjs drives a live argus headless (CDP) and asserts the laws — drawer-everywhere, zoom-completeness, lid parity, single-beckon-author, LANG toggle, runs honesty, the nameplate seal. Every scenario names the shipped violation that created it. `npm run smoke` against the attach-file daemon; 17 green at birth (and it caught the Esc stack's drawer-then-zoom order on its first run).
| 2026-09-01 (the nameplate seal) | Grilled and settled: surfaces carry marks as SEALS, never logos on chrome. The top-left block is the LCARS nameplate, and a mark there is engraved in the panel's own black — like its type, never full-color art (a color logo is a sticker; an engraving belongs to the panel). Rendered as a CSS mask over the vendored ChRIS SVG (src/assets/brand/), so one rule obeys every skin without variants — verified on Lower Decks and sickbay. The ChRIS mark names the SYSTEM in the nameplate; the INSTITUTION speaks through the palette (sickbay = BCH navy+cyan). The rule binds PHAROS too: same nameplate slot, same black. Sanctioned seal sites beyond the nameplate: the attach card and an idle face — nowhere else.
| 2026-09-01 (the pulse had two authors) | Operator re-report, upheld: the elbow-join pulse survived the CSS scoping because it had a second author — main.ts stamped `lcars-beckon` on the toggle element, which since the lid became the toggle is the WHOLE bar; the container's filter painted every segment. The verification had checked the children's computed animation and never the container's — the lesson lands next to the crop rule: verify the element that PAINTS, not just the ones you styled. The JS stamp and the dead class are gone; the closed-state beckon has exactly one author (CSS, end block only), re-verified at container, first-segment, and cap level.
| 2026-09-01 (a refused roster says so) | First catch of the 95k-instance stress identity: `proc feeds` is REFUSED during a cold warmup (the guard declines global queries over a half-built index), but the DAG pane only listened for roster models — the refusal was invisible and RETRIEVING FEED ROSTER… stood for hours. A loading row that outlives its request is a lie. The pane now answers a refusal with the daemon's own reason plus a live warming figure from the prompt context (verified ticking at 29%), and re-asks by itself when the index comes whole. Feed-scoped entry (`feed diagram feed_N`) was never guarded, so a known feed opens fine mid-warmup — WALL/SIZE verified live that way (19s / 264B on a real node).
| 2026-09-01 (metrics face the operator; no keyboard required) | Four moves, one law — every affordance must work by touch and read honestly. The node facts chip gains WALL and SIZE rows from the cached metrics (duration and bytes humanized); a settled node without backfill says `awaiting warmup`, an unsettled one says `in flight` — never a blank. The LANG given became a show/remove toggle (LCARS has no Esc key on a wall panel or a phone) and wears a lit butter dress while the line is on stage. The lid's beckon moved off the bar onto its end block alone: a brightness filter across the whole bar visibly detached its first segment from the static elbow arm it joins — on a live display the seam pulsed (operator catch, sickbay skin). And sickbay itself now flies the BCH flag: the ramp is anchored on bch-logo.svg's navy #164989 and cyan #1EBEF0 rather than generic blues.
| 2026-09-01 (the divider is a pair) | Operator nit, upheld (PR #306): closed, the header divider and the lid read as offset twins — the lid ended in its block cap, the divider ran clean to the frame edge. The theme pairs its bars (bar-1..5 over bar-6..10, bar-5 and bar-10 matching end blocks); our header markup had dropped bar-5. Restored — both rows now end harvestgold / black gap / block at the same pixel columns (verified), and the closed console reads as one symmetric sandwich.
| 2026-09-01 (the lid restored to canon) | The flattened lid band was itself a doctrine violation and PR #305 undoes it: the lid is the theme's own segmented divider again, identical open and closed by construction (the old defect was closed-state overrides wrapping the tail segments; state now changes only a beckon pulse — verified: nine segment boundaries pixel-identical in both states). The CONSOLE label died with it — the bar law admits no machinery text on a bar, so the WHOLE lid is the toggle: a lid opens by being touched. Three continuity nits fixed the same way: the palette's baseline now bridges glyph and cap instead of overrunning their curves, LANG wears its siblings' dress (the theme's padding rule enumerates panel-2..7 and had skipped it — hence the flush-edge label), and the console's 4px harvestgold rail died so console and workspace meet the gutter's edge on the same black line (both flush at the same pixel column).
| 2026-09-01 (LANG is a given + a control must read) | Operator interrupt caught two wrongs in one row (PR #304): the lid's toggle segment was an ILLEGIBLE control (a band that swallowed its own handle) — it now carries the label CONSOLE, black text on the orange cap, because a control must read as one; and LANG never belonged on the workspace — the command line is workspace-scope machinery, so it became a gutter given (butter, between SOURCES-04 and COMPUTE-05). The drag strip flattened to a quiet hairline (its radius and pulse read as a misaligned double line), and the theme's bar-border black slot on the toggle died. Crop-level verification is now the bar for border work — full-frame glances passed what crops condemned.
| 2026-09-01 (ranked stays ranked) | A pull in RANKED heated the molecule's whole-graph force simulation, dissolving the tiers into force equilibrium mid-drag — the operator read the aftermath as a broken ranked settle (PR #301). Ranked IS deterministic tiers, never a settle: same graph, same picture. Under ranked a pull now moves only the grabbed node (edges follow, neighbors frozen — verified 0 bystanders moved) and the release eases it home in 300ms; the elastic whole-graph reaction remains a molecule behavior.
| 2026-09-01 (honest pills + metrics backfill) | The TIME pill's silence diagnosed to the root (PR #301): a v1 checkpoint never backfills metrics (settled feeds never re-page, by our own cache law) — checkpoint schema v2 retires it, one warmup repopulates. Metric scaling now applies in RANKED too (a mode pill that changes nothing reads as broken); a mode with no data behind it DIMS and says so on its tooltip instead of silently rendering an unchanged graph; a local re-projection fires no wave (an unasked pulse reads as a glitch); and PULSE became the mode it always was — PULSE ON / PULSE OFF, lit when looping, dim when quiet, with the one arrival-wave on load kept as canon.
| 2026-09-01 (the lid is a strip) | The quiet-lid fix treated the symptom; the operator's screenshots showed the disease (PR #302): the lid reused the divider's segment vocabulary, so open-state stacked two near-twin bars with unaligned cutouts, and closed-state left the toggle floating as a lone RED block (red is for alerts). The lid is now a purpose-built strip — one thin continuous harvestgold band, identical in both states, the toggle an integrated orange segment that beckons only while closed, LANG riding the cap. A machinery row cannot masquerade as frame.
| 2026-09-01 (prefix everywhere + strip dress + quiet lid) | Three corrections from live use (PR #301): Ctrl-B is claimed unconditionally — the console exclusion protected a readline behavior the terminal never had and donated the key to the browser's bookmarks; the command line re-dressed as an LCARS strip (frame-docked bar with color block, black field, punctuation cap, console-sized Share Tech Mono) replacing the floating rounded dialog; and a closed console reads as ONE bar — the lid row's decorative segments step aside, leaving the beckoning toggle and LANG on black (two adjacent near-twin bars with unaligned cutouts read as a glitch).
| 2026-09-01 (the bar law) | Locked after a visual-language audit (PR #300): sizing and layout machinery NEVER rides a title bar — a bar is identity + state + the drawer handle, uniformly, console included. End-caps returned to Okuda punctuation: passive frame-colored terminators, never red (red on a bar is reserved for an actual alert, per the flight-deck color law and the dark-cockpit rule: at rest, annunciate nothing). The console reached parity through its own canon — lid toggles, strip drags, `console zoom|height` verbs — its butter zoom square and mars close left the bar, and the LANG capsule moved to the lid strip (the machinery row).
| 2026-09-01 (regression triage) | Three self-inflicted wounds from the language slice, live-verified fixed (PR #299): layout leaves carried `data-pane` (added for spatial focus) and the zoom handler's delegated click on `[data-pane]` turned EVERY pane click into a zoom toggle — leaves now carry `data-leaf`; Ctrl-B leaked to the browser (bookmarks) whenever the focused pane had no drawer — past the terminal exclusion the prefix is always claimed; and the split glyphs ◀▶▲▼ yielded to typography per LCARS canon: a SPLIT label with LEFT/RIGHT/ABOVE/BELOW pills.
| 2026-08-31 (the argus language) | Every gesture became a sentence (PR #295, grilled): subject-first verbs (`pane split right`, `view runs`, `dag projection 2d`, `back`) run client-side off reserved subjects, act on the focused pane by default with `@id` targeting from day one, and drive the SAME DOM controls the mouse does — fidelity by construction. Prefix-`:` (tmux tradition) opens the floating command line with Tab completion; the console understands the language too; the LANG capsule is the mouse handle. A DESKTOP is a saved layout in this language — a verb script with `%n` replay ordinals and no concrete addresses (the schema law) — `desktop save|load|show|list`, stored locally and written through to CFS via the new `config write` builtin (help-declared per the one-declaration gate), with honest fallback when the daemon predates it. Live-verified: a three-pane composition with linked-fs and viewer bindings round-tripped save → reset → load with shape, kinds, and bindings intact.
| 2026-08-31 (visible waits) | The RUNS-02 delay diagnosed and treated (PR #291): `proc feeds` is cache-fast — the wait is the serialized session queue, and the biggest ambient offender was the DAG pane following the cwd while OFFSTAGE, queuing silent `feed diagram` traffic nobody could see. Offstage panes no longer follow; the roster request shows a breathing RETRIEVING FEED ROSTER bar from the first frame. Express-lane queue priority stays shelved until a measured stall names a blocking command that cannot simply be removed.
| 2026-08-31 (drawer grammar) | The behind-pane controls became a grammar (PR #290, grilled): three color-coded groups — STRUCTURE (harvestgold: four placement splits ◀▶▲▼, ZOOM shown only when unzoomed, CLOSE), BINDING (butter radio: UNLINKED default, LINKED FS, LINKED VIEWER — a selector, not an action; SPLIT is the one verb that creates, the binding says what the created pane IS), and CONTEXT VERBS (orange, red for destructive). Recursive on every pane at every depth. DELETE runs as a visible `rm` — destructive actions are auditable commands. VIEWER and BROWSE NODE retired into the binding radio. Full section below.
| 2026-08-31 (honest framing + hygiene) | Live-diagnosed round (PR #289): the "black obstruction" and the 2D vanish were one bug — a naive framing block set camera distance past the fixed far plane on sprawling molecules, near-clipping voids in 3D and swallowing 2D whole; the bounding-sphere fit is now the one framer and the far plane always clears it. The immersive hop actually lands ('job' rows were classified as files). Selection facts and node selection clear when leaving a graph.
| 2026-08-31 (differentiation metrics) | Node size means something (PR #288): wall time and output bytes ride the warmup's own list rows into the proc cache and checkpoint (zero new CUBE calls), project onto `node.metrics`, and scale the molecule; the rail's SCALE pill flips TIME | SIZE by re-projecting the remembered model locally. Deliberately NOT cpu/memory — CUBE reports resource requests, not usage, and sizing by them would lie.
| 2026-08-31 (immersive truth + hop + subway) | The immersive root is the node itself (PR #287) — status, params, log, data, children — not its `data` link, so the label stays honest; clicking a descendant plugin instance inside a node is a HOP, not a descent: the camera flies out of this node and into that one, the overlay following; and the facts chip gained the subway strip — the classic ChRIS UI per-node lifecycle (created → waiting → scheduled → started → registering → terminal) as stops on a line, filled to where the job is, red terminal on error.
| 2026-08-31 (pulse legibility + flat discs + facts overlay) | Three display corrections (PR #285): the wave flares COOL (`--dag-pulse`, seaglass-cyan) with a size swell — a white flare vanished on the butter of finished nodes; 2D draws flat discs, not lit spheres; and the selection-facts chip overlays the scene inside the canvas instead of reflowing beneath it, so indicating a node never resizes the graph.
| 2026-08-31 (true 2D + rail taxonomy) | The 2D molecule is a genuine two-dimensional force settle (PR #285) — a 3D settle flattened afterwards piles nodes that resolved their overlaps in depth; the drag simulation goes 2D with it. The rail taxonomy sharpened (operator's refinement): the rail holds DISPLAY-CONTENT controls — layout, projection, PULSE, SCALE — while the drawer holds pane/session machinery.
| 2026-08-31 (dive polish + honest link) | The fly-in no longer snaps the graph before diving (PR #284): the camera aims at the node's world position from whatever stance the operator holds. And the rooted browser inside a node shows the truth: salsa's proc provider delegates the whole subtree under an instance's `data` link to its CFS target — listings, reads, binary reads — instead of silently re-listing the instance with broken paths.
| 2026-08-31 (rail canon + 2D pill) | Rail pills corrected to canon (PR #283): filled capsules with black text — the color IS the interface; the outline look was header-bar styling inherited onto the canvas. A 2D/3D pill joined the rail with a real projection behind it.
| 2026-08-31 (orbit + rail + honest fit) | Three DAG-pane corrections (PR #282): a press no longer snaps the view — the spin pauses in place, empty-space drag orbits (shift/right-drag pans, wheel dollies), a node drag still pulls the structure; view-state pills moved off the header bar into an in-pane rail; and the scene owns a ResizeObserver so a pane resize refits the render buffer instead of stretching the graph.
| 2026-08-31 (live-verified polish) | First round fixed by looking at the live surface headless (chromium + CDP against the running daemon) instead of guessing from operator screenshots (PR #281): RUNS-02 restored from under the theme's corner-mask rectangle (every gutter segment now positioned above it); full width recovered (the desktop flex column had shrunk the theme's auto-margin wraps to content width); the cycler refits via ResizeObserver; the console's resting height dropped to 28vh. Flow verified by driving the page: RUNS-02 → full list → click → full DAG → Esc → list.
| 2026-08-31 (header diet) | The header's budget is ~20% of the screen and it must earn even that (PR #280): the banner shrinks to bar-scale, faces pack dense (three-column telemetry, compact version rows), the pipeline cycler rides small beside the data instead of headlining, attribution is one dim line, and a 19vh face guard caps the whole. Rationale: identity is candy, versions are a summonable face (and a console command), telemetry earns resting-state space only when dense — the workspace is the point of the page.
| 2026-08-31 (elastic gutter) | The gutter's givens became elastic segments (PR #279): the theme's fixed viewport-fraction button heights summed past any real window once the page stopped scrolling, clipping the bottom presets off stage. The column now shares its height in the theme's visual proportions with legibility floors — every given always on stage. Scrolling the gutter was considered and rejected on the same ground as page scroll (a preset hidden behind scroll is not a given); if the roster ever outgrows compression, the LCARS answer is a discrete bank flip, not analog scroll.
| 2026-08-31 (edge pixels) | A zoomed pane owns every pixel (PR #278): the theme's fixed black trim strips (headtrim/baseboard) and the header's lid no longer cover a zoomed pane's first and last content rows; in header-away state the lid's height is reserved above the workspace instead of overlaying it. Page-scroll formally ceded: the composition is a declaration (preset + header face + drawer height), scroll position is hidden state, and content depth lives inside instruments.
| 2026-08-31 (yield order + select=enter) | Two corrections from live use (PR #277): the desktop contract gained its yield law — when header + console outgrow the viewport the DRAWER shrinks, never the workspace, which keeps a 20rem floor (the frozen drawer had crushed panes to a sliver); and selecting a feed in the RUNS-02 list now ENTERS the full graph (list steps aside, Esc returns) — the half-pane preview-above-the-list is gone, with live scrubbing reserved for a future context split.
| 2026-08-31 (console face, settled) | Third pass locked the shape (operator correction): the boot stays the classic in-scroll experience — brain lit at connect, boot log and in-stage progress beneath — and at ready the screen is replaced by a MINIMAL face on the alternate buffer: the animating brain, one status line (DAEMON RUNNING · SESSION ESTABLISHED / ENGINE EXECUTING), and the Esc hint. Nothing else. Esc = the verbatim text record (boot log, addresses, token); any key returns. The ready handoff halts the boot pulse without a final repaint, so the text record is never stamped over. Earlier two-phase/panel design (PRs #271/#273/#274) superseded by PR #276.
| 2026-08-31 (desktop contract) | The workspace declared a desktop, not a document (PR #272): the shell owns the viewport (100dvh, page never scrolls), the main region flexes into what the header and console leave, every pane fills its leaf as a flex column, and scrolling happens inside instruments — never on the page. The fixed viewport fractions (42vh interior caps) are gone; a full-workspace preset really is full-workspace.
| 2026-08-31 (contextual back) | Esc reframed as an Android-style contextual back: per-pane navigation stacks (feed list → graph → node immersion), one pop per press, transient chrome (drawer, then zoom) before navigation; preview replaces the stack top while dblclick-enter pushes; RUNS-02 declared as always landing on the pure feed roster (no ghost renders). Navigation macro language (text commands driving the same intents from the embedded terminal) sketched, gated on its own grill.
| 2026-08-30 (pulse + fly-in) | Two DAG behaviors grilled and locked: the pulse wave (dependency-order replay; a join fires only when all parents have; history-honest — the wave halts at the execution frontier; drawer verb plus one wave on load, always-on only in the ambient miniature) and the fly-in (dblclick dollies the camera into a node and overlays a rooted browser in the same pane; Esc exits the node in one press; the overlay shares the DAG pane's group so regard writes feed its slaved viewers; ENTER NODE stays in the drawer as the session-global cd).
| 2026-08-30 (PHAROS) | LCARS grounded properly (what it is, why chosen, whose property). Longevity strategy settled: PHAROS house language as default (chamfer, beacon, harbor palette; recursive name — Presentation House for AEGIS — and the six-chapter inventory), LCARS demoted to operator-supplied skin via theme import, functional carve-out grounded in _Lotus v. Borland_ / _Apple v. Microsoft_. Naming law recorded: kitchen names never expand; harbor names expand through the layer below, grounding at CALYPSO's fixed point; ARGUS coined the All-Regarding Graphical User Surface.
| 2026-08-30 (slice 1 built) | Regard + drawer + slaved viewer implemented: `regard` wire message (menu), brasa session retention, calypso retain/mirror/rebroadcast with late-attach replay, argus SubjectBus (groups, retained cells, LWW), pane drawer behind the header on all stamped panes, VIEWER semantic child (slaved) and BROWSE NODE (rooted) in the drawer, viewer pane kind subscribed to group regard.
| 2026-08-30 (later) | Subjects grilled and locked: two-layer regard (group-scoped rendering + session aggregate in brasa), retained-cell write semantics, rooted-versus-slaved children, lifecycle (groups session-only, session regard survives surface reload). Durable layer settled in direction: per-user config and named layout schemas as CFS files under `~/.config`. Build of the first slice (regard skeleton, pane drawer, slaved viewer) begun.
| 2026-08-30 | Document created from the first AEGIS thought sprint. Settled: the machinery-behind-the-frame principle (pane drawer as the mouse-native prefix key), semantic children as parent-contextualized pre-composed intents, pane linkage as hub-and-spoke subjects (never pane-to-pane edges), the addressability test, and *regard* as a session-truth subject in brasa. Open: palette roles, shape grammar, motion canon, the violations audit.
|===

== The laws (machine-checked)

The components a pane declares into are documented one section each in `components.adoc`, the reference beneath this doctrine. Doctrine without teeth decays into apology. Every law below is enforced mechanically: `lint:` checks run in `scripts/aegis-lint.mjs` on every pull request; `smoke:` scenarios run in `apps/argus/tests/smoke/smoke.mjs` against a live surface. The lint gate also verifies this table itself — a law whose named enforcement does not exist fails CI, so new doctrine cannot be written without teeth.

[cols="1,3,1"]
|===
| Law | Statement | Enforcement

| drawer-everywhere | Every workspace pane — template-stamped or static — carries the pane handle and the three-group drawer. | lint:drawer-everywhere
| bar-no-machinery | No machinery rides a title bar: no buttons, roles, or control text inside a bar beyond the handle itself. | lint:bar-no-machinery
| caps-are-punctuation | Every bar end-cap is passive frame punctuation: the punctuation class, no text, no role. | lint:caps-are-punctuation
| lid-is-mute | The lid's segments carry no text — the lid is punctuation, and the whole lid is the toggle. | lint:lid-is-mute
| single-animation-author | An animation has one author: CSS declares it; script never stamps an animation-bearing class. | lint:single-animation-author
| lid-parity | The lid is identical open and closed; only the end block may breathe, and only while closed. | smoke:lid segments identical
| zoom-immersive | A zoomed pane owns the stage: header, gutter, lid, and status offstage; only the thin restore strip remains, and it restores. | smoke:zoom slides the whole header
| given-idempotent | A gutter given always renders its target; it never toggles. | smoke:a given never toggles
| control-reads-next-action | A toggle control reads as what it will do next (ZOOM/RESTORE, open/close). | smoke:the capsule reads RESTORE
| a-wait-over-two-seconds-shows-progress | Anything that changes the surface and takes more than two seconds shows its progress where it happens: a bar that moves, or — when the end cannot be counted, as a question to the session — the words for what it is waiting on and a running bar. The work runs in slices, so the page can draw the bar; a wait with nothing on screen reads as a dead surface. | smoke:a wait over two seconds shows its progress
| honest-wait | A wait or refusal is always visible and truthful; a loading row never outlives its request. | smoke:RUNS-02 answers
| touch-first | Every affordance works by touch: no keyboard-only control. | smoke:LANG opens the line
| ordinals-mark-channels | A gutter ordinal (`-0N`) marks a summonable domain channel, numbered sequentially down the column; meta givens (LANG — the language, not a channel) carry none. | lint:ordinals-mark-channels
| a-click-does-not-resettle-the-space | Indicating a node changes only the node's paint: no click re-settles the graph or refits a camera the operator has placed; a repaint of the same space keeps that camera. | smoke:a click keeps the camera
| focus-citizenship | The prefix key addresses whatever the operator last touched — and the console is a citizen of that model like any pane. | smoke:focus citizenship
| caps-are-the-sort | A tabular pane's column caps are its own frame: touch a cap to sort, again to reverse, the active cap lit; the frame stays anchored while the rows scroll, and each cap's label sits where its column's text sits. Filtering is a mode summoned from the mode frame's FILTER block or the language, and the title bar carries its state (`FILTERED n/m`). A grouped listing heads each group with its own caps, minted from the same declaration and lit together: one sort, one filter, however many groups are on stage. | smoke:column caps sort
| console-joins-the-grammar | The console is a pane that does not split: the same handle and drawer as every pane, with only the verbs that apply — no SPLIT, hence no binding. | smoke:console drawer
| nameplate-seal | Marks are engraved seals in the nameplate's own black — never full-color logos on chrome. | smoke:nameplate seal
| select-answers-at-once | A selection answers before its data does: the list steps aside, the pane names what it is retrieving, and the bar reads LOADING until the answer lands or the refusal shows. | smoke:selecting a feed answers at once
| enter-lands-in-a-place | ENTER moves the session into a directory, never a file: a regarded file resolves to its directory, a regarded node to its data, nothing regarded to the feed on stage; the roster pick offers ENTER FEED. | smoke:ENTER FEED moves the session into the feed
| bin-is-browsable | A /bin entry opens as context, not a dead row: everything there is a graph — a pipeline as its summary with its authored DAG, a plugin as the one-node graph it is — both on the same stage, with the same CLOSE a file view has, and no wall of scraped text anywhere. | smoke:a plugin opens as a graph of one node
| a-view-is-closed-by-the-operator | A listing arriving for a pane whose operator is reading something lands UNDER it; CLOSE and Esc then show the newer one. A view taken away mid-read loses whatever was fetched into it, and a click that opens a view is not a request to close it. | smoke:a view is closed by the operator, never by an arriving listing
| projection-is-a-mode | A listing pane draws its one listing in more than one way — rows, cards, or cards with previews — and the way is a mode-frame capsule that reads as the current state; sort, filter, and activation are the same under every projection. | smoke:CARDS projects the same listing
| following-is-declared | A browser bound to the session cwd says so on its bar (`CWD`); the binding is a verb both ways (FOLLOW CWD / ROOT HERE, `file follow` / `file root`), so any browser can be the console's and the console's can be frozen. A binding nobody can see is a hardcode. | smoke:the following browser says CWD on its bar
| pin-survives-promptlines | An operator's pick holds until the operator moves: a cwd-follow answers a change of cwd, never the promptline every command brings, so a session parked inside one feed cannot replace a pick of another. | smoke:a pick survives promptlines while the cwd sits elsewhere
| hover-never-glares | A hover brightens a control in its own hue; no hover rule reaches for the palette's near-white. | lint:hover-never-glares
| roster-grid-single-source | A roster's columns, gap, and edge padding are declared once on its host and read by caps and rows alike; no roster grid spells its own template. | lint:roster-grid-single-source
| canvas-fills-its-mount | A scene canvas fills its mount in CSS pixels at any device pixel ratio: the scene pins its own canvas, never a per-host stylesheet; a content view never spills sideways. | smoke:the content view never spills sideways
| host-control-is-annunciated | A daemon acting on its own host is never silent about it: the HOST lamp reads the attach ack's declared tiers, present exactly when declared, absent at rest. | smoke:the HOST lamp reads the daemon
| hue-is-a-mode | What colors a node is a declared mode on the mode frame (STATUS, COMPUTE), and a mode's legend travels with it on the bar; errors are visible under every hue. | smoke:the HUE block reads STATUS
| field-is-content | A pane's field carries content only: display-mode capsules live on the mode frame, no control floats over the field, and the repealed floating rail's enforcement is gone. | lint:field-is-content
| deferred-warmup-failure-persists | A warm-up moved off the boot gate reports its failure afterwards, by name, and keeps reporting it until a later attempt succeeds — a boot readout has scrolled away by then, and a message that vanishes is one nobody acts on. | smoke:a failed deferred warm-up is named on the status readout and persists
| index-movement-never-holds-the-lane | A command that needs a topology the cache does not hold answers at once that the feed is indexing, and the walk runs off the session's serialized lane: every other command from every surface goes on answering while a large feed indexes, and the graph follows on the feed's watch when the walk lands. | smoke:a cold walk returns the lane at once
| index-movement-is-annunciated | Index movement no command announces — a feed's first-visit load, a roster arrival, the global sweep — reaches the JOBS readout and the prompt while it happens and clears when it is done; a walk of a minute never reads as a hang. | smoke:the JOBS readout names a feed being indexed
| frame-retracts-to-a-strip | A frame that gets out of the way leaves a thin strip — the header, a pane's mode frame — and touching the strip brings it back; touching the field or Esc retracts a mode frame; the bar annunciates the non-default modes meanwhile. | smoke:the mode frame rests as a strip
| surface-never-shadows-the-session | The argus language runs before a line reaches the session, so a subject that is also a kernel command must claim only the verbs the session lacks and declare itself shared; every other subject collides with nothing. A surface that eats `pacs query` has taken the operator's instrument away. | lint:surface-never-shadows-the-session
| a-node-opens-as-what-it-is | Activating a node opens what it is made of, in the surface's own `text : detail` idiom, and Esc retreats exactly one level — out of the node, not out of the view holding it. A node that carries nothing says so, because an empty panel reads as a failure to load rather than as a thing with nothing in it. | smoke:diving in opens the node as its parameters
| one-frame-answers-the-field | A pane has one mode frame, and its blocks answer to what the field currently holds: a listing offers its projection and filter, a diagram offers the modes a graph has, and neither offers the other's. A control that cannot act is a promise the frame does not get to make. | smoke:a diagram on stage swaps the frame
| a-wave-is-a-verb | Motion is asked for, never ambient. The dependency wave runs one pass when a hand presses it and the control reads as the verb, because the dark cockpit animates nothing at rest — the header's ambient cycler remains the single deliberate exception. | smoke:PULSE is a verb
| a-preview-is-the-same-drawing | A preview and the thing it previews are one drawing at two sizes: the placement is declared once and each medium only paints it. A card may paint SVG where the stage paints three.js — a grid of ninety previews cannot hold ninety WebGL contexts — but it may not compute its own tiers. | lint:a-preview-is-the-same-drawing
| provenance-is-annunciated | An answer that was not fetched now says when it was — the date it was answered and how long ago — and the control that would replace it reads as what it will do next, lowering to a command the operator can see. A fresh answer says nothing, because the absence of the claim is the claim. The surface states the age and never acts on it. | smoke:a replayed answer says when it was answered
| a-form-stands-where-its-answer-will | A query form's fields sit on the listing's own grid, in its caps' order, so a term is typed in the column it will fill and the line it lowers to reads as the listing reads. A column the query cannot narrow on carries no control at all — a field that silently does nothing is worse than a gap. | smoke:the query form stands on the listing grid
| listing-is-one-abstraction | A tabular pane declares its listing into the `Listing` façade and never builds one: columns are traits declared once for cap, cell, order and track; a row's verbs are actions the façade seats in the track it reserves; the frame that sorts and filters, the indication, the readout and the selection are the façade's. Reaching past it — to the order, the host, the row or capsule builders, or a hand-typed `--roster-cols` in the stylesheet — is the violation, since that is how three panes came to spell one table three ways and then one indication twice. The PACS pane carries the one named allowance, printed as debt on every run, until S4 converts it. | lint:listing-is-one-abstraction
| a-selection-belongs-to-the-field | SELECT is a mode: while it is on a click gathers rows and row verbs stand down. The selection is the field's — it survives a filter and a re-listing, it is cleared by navigation, and leaving the mode keeps it — and its verbs ride the frame's row zone, each lowering to ONE command over many operands that asks once for the whole set. | smoke:a bulk act is one command the operator could have typed
| a-row-is-indicated-before-it-is-acted-on | A listing that hides its verbs until a row is indicated must distinguish indicating from activating: a click indicates, a double-click activates, and a row with no verbs to hide — or a listing with none — keeps its single click; a row with a control keeps navigating apart from selecting: the control activates (folds, enters, opens) and selects nothing, the rest of the row selects and does not activate. Where the verbs stay on the row, the track that holds them is reserved on every row at one declared height, so indicating changes what the track holds and never the geometry around it; where they ride the frame there is no track to reserve; and every row verb lowers to a session command the operator can read. | smoke:no row moves when one is indicated
| process-acts-on-a-place | PROCESS is offered on a place — a directory outside a feed, a node's own data inside one, a DAG node, a PACS series or wholly-home study — never on a file, never beneath a node. It opens `/bin` as a catalogue bound to that place, joined to the pane's group; RUN there lowers to the one line the console would take, echoed, with a new feed's title asked once. | smoke:PROCESS opens a catalogue bound to the place, and RUN schedules a run on it
| a-row-s-verbs-live-in-the-frame | A row's verbs are drawn in the frame's row zone, never on the row: a listing that declares the zone reserves no track, the indicated row is drawn as a segment of the frame (its block colour, black type, run to the spine), verbs arriving open the frame and the last of them leaving retracts it unless the operator holds it, and the frame retracting stands the row down. A listing holds one indication across its levels, and it belongs to the field: a repaint of the same field restores it, a row that leaves takes it, navigation clears it. A row's control activates without selecting, and its body selects without activating; a selection's verbs ride the same zone. | smoke:a row's verbs live in the frame's row zone, one beneath another, and the frame opens as they arrive
| a-readout-that-acts-says-so | A readout that can be pressed wears the press as a capsule, says what the press does, and acts on the listing the operator can see: the header's ERRORED figure opens the runs roster filtered to the feeds it counted, and never acts on a pane that is not on stage. | smoke:the ERRORED figure opens the roster filtered to errored feeds
| a-control-lives-where-it-acts | A control sits with the thing it acts on: the drawer acts on the pane, the mode frame on what the field holds, a row's track on that row. A verb that acts on a row never rides the pane's drawer, and a control that cannot act on what the field holds stands down rather than misleading. | smoke:a browser's drawer has two groups
| an-absent-value-is-a-question | A value the command needs and was not given is asked for, never guessed and never answered with a usage line — a flag given no value and a missing required operand are the same sentence. The ask says where to look and, only when it has one worth offering, what to offer; a verb whose only candidate is the operand it already has offers nothing. Abandoning it does nothing and says so. | test:fs-destination
| an-ask-borrows-an-instrument | A question for a value that lives in a navigable space opens the pane that already shows that space, beside the pane that asked, closing when the errand ends either way. Its own controls ride a bar of its own — question, answer, one committing verb reading what it will do — never a dialog, and never the mode frame, which answers to the field. The browsing that answers a question runs beside the command that asked it, never behind it. | smoke:a location ask opens a browser beside the asker
| a-question-is-answered-where-the-session-speaks | A question the SESSION puts to a surface is stated in the console, answered there, and kept in the scrollback — and a closed console exposes itself to carry one, since a question nobody can see is a question nobody can answer (a question the SURFACE puts stands on its own pane instead: `a-question-stands-where-it-was-asked`) — never in a box, and never silently refused. A secret is masked and never enters the transcript; a yes/no is two capsules; a question outranks the command line while it is open; and Esc abandons, which the asking command is told about rather than left waiting on. | smoke:the session can put a question to this surface
| a-server-is-named-never-swept | Choosing which PACS to ask is the FILTER gesture, not a dropdown: a cell that reads its own state unfolds a strip of segments, lit when included, retracting to the field or to Esc. No control offers ALL — a fan-out across servers of unknown liveness is thousands of questions into the void — so a fan-out is always something the operator named. One chosen is a context and moves the session; several is a query and moves nothing. | smoke:the strip offers no ALL
| a-set-says-what-became-of-every-member | A listing that answers for a SET names every member of it and what became of each: found, none, and could-not-ask are three answers, not two. A miss reads as a zero and a failure never does — a zero says the PACS answered, which is the one thing a timeout did not do — and the counts a surface reads out are derived from the rows, so a summary cannot disagree with what is on screen. | smoke:every patient asked gets a row, misses included
| progress-sums-its-levels | Progress on a row that stands over other rows is the sum of theirs, never their average, and a row with nothing scheduled still carries a dim track — an absent bar reads as "no such thing" where a dim track reads as "nothing has happened yet". | smoke:a study heads its series with caps and a summed track, and the series verbs live in the frame
| pane-is-the-subscription | A pane showing a running feed holds a watch on it and releases it on leaving; the bar reports LIVE / SETTLED / STALE; the kernel never polls CUBE on its own. (The calypso daemon reading its own code on local disk once a minute, to know whether it is stale, is not CUBE traffic and does not break this.) | smoke:entering a feed reports its liveness
| a-daemon-is-restarted-by-its-own-operator | Nobody restarts a user's calypso daemon from under them. Behind a door, the operator restarts their own: the RESTART pill beside LOG OUT, or the cure in the out-of-date notice. Both ask first, in the notice's spot, naming what a restart would cut off right now. The door does it and keeps the login, and every other surface is told why it dropped. | smoke:behind a door the RESTART pill asks first
| a-component-lands-with-its-reference | A component of the surface — a thing a pane declares rather than builds — lands with its section in `components.adoc` in the same pull request: purpose, anatomy (drawn and labelled, and photographed in each state once on stage), the declaration, its states, the laws it carries, its stylesheet hooks, and what not to do with it; and every slice that changes the component amends the section. Doctrine says what the surface is; the reference says how a pane gets one, and a component without a reference is a private API wearing a public name. | lint:a-component-lands-with-its-reference
| a-fact-has-one-source | A fact the surface needs in many places is written once and read everywhere: the four sides and all they imply (`app/sides.ts`), the pane bar's states and their one writer (`features/roster/bar.ts`), the primaries and the pane kinds as a union (`app/panes.ts`), the chords (`DRAWER_CHORDS`). The lint holds every other file to those sources; a re-spelling fails CI. | lint:a-fact-has-one-source
| a-part-stays-a-part | No function runs past 150 lines: about what one tall screen shows whole. A module factory (`*_wire`, `*_build`) is a boundary, measured by the named functions inside it. A function still past the ceiling is named in the lint with its reason and the length it stood at when the ceiling arrived: it may shrink, never grow, and a new one fails CI. A file past 1,200 lines is named as a warning, never a failure. | lint:a-part-stays-a-part
| a-question-is-answered-where-the-hand-is | A question the surface asks stands on the pane that asked it and on the console's input line at once; either answers it, the first answer wins, and the other place closes. The console's transcript records every answer (a secret masked). A line typed at the prompt while a question stands is the question's answer, never a command. | smoke:a pane question stands on the console too
| keys-are-documented-from-one-table | The chords have one source, `DRAWER_CHORDS` in the console language; everything that shows them reads it — the host (the keys, the capsule titles), the console (`argus keys`, `help keys`), the HELP pane (its rows, and the dashboard's HELP tile) and the written reference `docs/keys.adoc`, which a script generates and the lint holds to the table. Nothing about a key is written by hand twice. | lint:keys-are-documented
| a-drawer-verb-has-a-chord | With a drawer open (the prefix pressed), one key presses one of its capsules; the arrows focus the pane in that direction. A chord is a key on a capsule, never a verb of its own: the table is `DRAWER_CHORDS`, one source, read by the host for the keys and the titles and by the console for `argus keys`. A key typed into a line is never a chord. | smoke:a chord presses a drawer verb
| a-frame-is-reachable-by-keys | Whatever a pane's mode frame offers, the keys reach. Over a listing, with nothing else holding the keys, ↑↓ (Home, End) move a row cursor, Enter indicates the row so its verbs ride the frame, and Enter again goes, as the mouse's second press does: through the row's control cell where it declares one (a study folding open its series). → or Ctrl-B r takes the keys into the frame, its first verb focused as soon as the frame shows; Tab or ↑↓ walk the verbs, Enter presses, ← or Esc leaves. Esc in a pane's own line (the PACS form) gives the keys back to the stage, and a focus move by the prefix (an arrow, o, ;) takes them out of the console. Ctrl-B & closes every pane on the stage (`pane close all`). | smoke keys-reach-the-frame; unit keysListing.test
| a-pane-can-be-moved | A pane on stage can be moved to a side after it was split there. The drawer's SPLIT and MOVE pills say what the four direction capsules do: under SPLIT (lit at rest, sticky) a direction opens a new pane; under MOVE (one press, then the drawer falls back to SPLIT) a direction moves this pane — detached, re-split onto the nearest block on that side (its former sibling when there is none), even — so every outcome is a tree a split could have made. A side it cannot go dims before the press; the mover keeps focus; its bar reads MOVED <SIDE> for a moment and the console carries nothing from the capsule; `pane move <side>` is the same verb typed, refused by name. | smoke:a pane moves where the drawer's MOVE sends it
| a-red-is-never-pre-existing | The full smoke runs every night against a fresh private daemon as the e2e identity (`scripts/smoke-nightly.sh`, the `mise-smoke.timer` user unit; `make smoke-report` reads the ledger), at the desk width, and the listing stages again at a tablet width and under a finger. A red is a defect or a stale test and is read the next day; it is never carried as "pre-existing" — four were, for days, in 2026-10 (#899), and a red nobody reads is a test nobody has. | the ledger at ~/.local/state/mise/smoke/ledger.txt
| a-verification-sees-two-widths-and-a-finger | A live verification of a surface change is made at the desk width and at 1024 px, and under a coarse pointer, with real browser input (CDP mouse and touch, never synthetic DOM events alone); a session-affecting change is verified with a second identity. One daemon, one identity, one width and a mouse missed the squeezed PROGRESS column, the unfindable grips and the two-identity confusion in a single day. | smoke-nightly tablet and finger runs; SMOKE_WIDTH/SMOKE_TOUCH in the driver
| a-scenario-runs-a-line-to-its-prompt | A smoke scenario that types a line at the console waits for that line's prompt before the next step, never a fixed sleep: the console declares `data-busy` on its element from a submit until the prompt returns, and the harness's `console_run` types only on an idle console and returns only when it is idle again. Fixed sleeps raced CUBE's pace: the same three-scenario run, started three times at once, gave a crash, a red and a green (2026-10-06, #919), and the nightly's "question in the wrong directory" was a `cd` that had not landed. A scenario also leaves the console idle, and the hygiene check says when one does not. | smoke stage_leaks (busy); console_run in the harness prelude; terminal.ts busy_set
| a-feed-has-one-view | A feed is one view, whichever door opens it: the RUNS roster opens it at once, the universe flies into it, and both draw it with one builder (`scene/feedGraph.ts`), frame it with one set of verbs (`features/feed/view.ts`: arrangement, projection, draw, pulse, metric, census, gravity, hue, and the feed's note, tags and name), and stand it in the modes this device last chose, whichever door turned them. A feed opens as its tree, root on top, unless the device chose a molecule. The universe keeps only what is about the whole space (its arrangements, replay, saved positions). Before, the descent drew the same `feed.dag` with its own builder as a free-form burst while RUNS drew a top-down tree with other pills — the operator: "the runs and universe rendering should both come from the same source of truth and same rendering expectation". | smoke feed-one-view (one feed through both doors: tree root on top, frame words, facts, a mode turned through one door standing through the other; orrery publishes `data-roots-top` on the canvas); unit feedGraph.test, feedView.test, feedFacts.test
| a-secret-is-never-on-the-command-line | A password or a token is never typed on a command line nor written into a script: it is in `ps` for every user of a shared host and in the file forever. A secret lives in the user's own file at mode 0600 (`~/.config/chell/doors/<door-host>.json`, written by `chell auth login`), or in an environment variable a runner injects (`CHELL_DOOR_TOKEN`), and a file that is loose (not the user's, or readable by others) is refused by name, never used, and never a reason to fall back to a prompt a script would hang on. The nightly seeds its daemon's token from a 0600 config and pipes a minted token on stdin for the same reason. | chell auth (doorFile.test: loose file refused); smoke-nightly door step; porter --mint prints once
| a-token-dies-on-its-day | A door token has a fixed horizon (`PORTER_TOKEN_DAYS`, 30) from its minting and no sliding refresh: a leaked key in steady use still ends, and every script is re-blessed by a human on the day. From seven days out every call says so on stderr; at expiry the door refuses by name and chell prints the words to run, never a prompt. A token is revocable by name (`porter --revoke`, `chell auth logout`), a running door notices a store changed under it, and a token never mints a token. | porter tokens.test (expired refused by name); chell door.test (the warning and the refusal); routes.test (revoked refused on its next call)
| a-login-never-lands-on-an-older-kernel | The door mounts current code. Calypso writes the versions it runs into its berth at boot; porter, at every login, compares them with what it ships and, where the daemon is behind, ends it and boots a fresh one on the way in (saved state kept) before mounting. Running sessions are left alone — the stale warnings in ARGUS and chell cover a daemon whose code moved under someone mid-work — but nobody arrives on yesterday's kernel. | porter routes.test (a login onto an older daemon restarts it); scripts/smoke-upgrade.sh nightly (a published N−1 daemon, this checkout's porter: one restart, then attach; catches the #913 versions drop)
| the-expanse-is-never-sized-by-hand | A listing's columns may be sized by hand, the way a file manager's are: a drag on a grip moves the boundary under it with the pointer, and the two columns it separates trade the width, each within its declared floor; a boundary never moves against the hand, and stops where a side has nothing left to give. A double-press gives both columns back, the frame's COLUMNS block gives all back, and what was sized is the device's memory (`localStorage`), since a tablet and a desk want different widths. The one `1fr` expanse is never given a width: beside a boundary it gives or takes the difference while it has room to stretch, so the alignment the expanse rule bought — bars at one x across levels — survives any sizing; pinned at its minimum (a row wider than the pane) it is moved like a fixed column, and only that minimum changes. On a row already wider than its pane (a tablet) the neighbours are all at their minimums and have nothing to trade, so the column left of the boundary simply grows and the row grows with it, as Finder's and Explorer's list views do; shrinking still stops at the column's floor. (The first build sized the column left of the grip and let the expanse absorb: with the expanse on the left, as NAME is on a files listing, a column grew away from the hand and the NAME boundary did nothing — the operator, 2026-10-06: "completely and totally counterintuitive".) The block reads `COLUMNS · SIZED` while anything is sized (a-readout-that-acts-says-so). | smoke listing-columns; unit columns.test.ts
| a-row-never-runs-under-the-rail | A listing's row ends inside its field: when the pane is too narrow for every column, the columns that declare a rank give way lowest first until the rest fit, and the bar says how many stood aside (`n COLUMNS HIDDEN`); nothing is clipped without a word, and the frame's corner never covers a cell. A wider pane brings them back. | argus `listing` (`listingState_compose`), live measurement at 1333×833×1.5
| a-readout-names-the-release-it-reads | Every surface of the release notes says which release it reads: the NOTES pane's bar names the daemon's installed versions, the dashboard's WHAT'S NEW block reads AFTER RESTART when the daemon's disk has moved, and the stale-page notice's one line of news is the page's own bundle's — the page being the one ahead. A terminal attached to a stale daemon says so in the same words, with its own newest headline. | smoke notes, stale-page; chell remoteClient.test
| a-field-says-it-holds-more | Every scrolling field hides the browser's bar and wears one chip at its foot — `▼ N MORE` while content lies below, `▲ TOP` at the end — through the one helper, `more_wire`. MORE_FIELDS (features/roster/more.ts) names every field that scrolls, by the class or id its stylesheet rule names; the lint holds every `overflow: auto` rule to that list and every name on it to a wiring. A container whose child is the wired field does not scroll itself. The console's scrollback keeps the terminal's idiom, by name. A field also answers its edges: a scroll come to rest at the top or the foot, or a wheel or a finger pushing past one, flares that edge in the theme's hue (`--edge-flare`) and gives the field's contents a small bounce, as a phone's list does; the script says only which edge (`data-edge`), the stylesheet draws it, and nothing moves under reduced motion. | lint:a-field-says-it-holds-more
| an-instruments-field-is-foreign | An image pane's field is a guest engine's — its pixels, its tools, every behaviour behind them — and the frame around it is native: one block per library tool, the layout, the colormap, SAVE and the FOCUS mark ride the pane's mode frame like every mise control, and nothing sits over the image. The old viewer lost on behaviours, not buttons; so the library keeps the behaviours and mise keeps the buttons. | lint:an-instruments-field-is-foreign
| an-editors-save-is-a-line | An EDIT pane's field is a guest's (CodeMirror: the typing, the undo, the search, the syntax colours) and its frame is mise's, as an image pane's is: SAVE and REVERT ride the mode frame, lit only while the field differs from the file, FOCUS says the field holds the keyboard, and the bar says DIRTY or SAVED. A save is the line the operator could have typed, `touch --withContents='<text>' '<path>'`, echoed in the console and run through the kernel, so the transcript holds every save and a feed's note is written through its projection like any file; the pane reaches no byte route. The kernel's `edit` opens the pane and is answered at once that it opened. A second `edit` of the same file finds its pane and leaves unsaved changes standing. | lint:an-editors-save-is-a-line
| a-holder-is-a-mark-and-a-line | Who can read a feed besides its owner — each user, each group, and PUBLIC when it is — are marks on its indicated row's readout, each with a × that asks on the pane ("Stop sharing feed_N with …?") before it withdraws, by `setfacl -x u:`/`g:`, or `setfacl -m o::-` for PUBLIC. SHARE asks a choose question: the CUBE groups the operator belongs to and EVERYONE as pills, the ones that already hold dimmed, a field for a user; each pick runs `setfacl -m`, and the question comes back until DONE. RENAME asks for the name with the one it has, then writes `/proc/jobs/feed_N/title`. Every change is a visible line. | lint:a-holder-is-a-mark-and-a-line
| a-tag-is-a-mark-and-a-line | A feed's tags are marks after its title in the runs roster and on its graph's frame, one theme hue for every tag (a colour CUBE stores is not painted), the listing's own font size. A press on a mark filters the roster by it (`tag:`); its × takes it off. Every change is a visible line: TAG's question (the user's tags as pills, the worn ones dimmed, a field for a new one, asked again until DONE) runs `setfattr -n tag`, after `mkdir /proc/tags/<name>` for a new tag, and × runs `setfattr -x`. A feed's note is read only when the feed is entered (CUBE's feed list says nothing about notes): the graph's frame reads ADD NOTE or EDIT NOTE and shows the note's first line; the roster offers NOTE, which opens the editor. | lint:a-tag-is-a-mark-and-a-line
| focus-stays-in-the-field | A click in an image field takes the keyboard and keys stay there — the wheel scrolls the stack, never the page — until Esc gives it back, one press, one level; the frame's FOCUS mark says who has the keyboard. A verb typed in the console drives the pane whether or not the field holds focus. | smoke:the image field keeps the keyboard until Esc
| tags-follow-the-image | A tags pane is slaved, never detached: it lists the elements of the instance the image pane in its link group has on screen, re-asking the kernel (`dcm tags`) on every slice the image pane regards, and it never lists a file on its own account except the one a `dcm tags` claim handed it. Identifying values arrive flagged from the kernel and show redacted until the REDACT block on the frame says otherwise; the kernel strips nothing, the surface chooses. | smoke:the tags pane follows the image pane's slice
| an-annotation-is-a-file | A measurement made on an image is a DICOM object (an SR now, a SEG when HARBOR decides), filed under `~/annotations/<SeriesInstanceUID>/` through the surface's own route and found again by the kernel by that UID — never by the path the series happened to be seen at, since one series lives under the PACS tree and inside every feed that copied it. What SAVE wrote comes back onto the field when the series reopens, wherever it reopens; `dcm series` lists it; `ls` finds it; nothing invents a format. | smoke:a measurement saved as an SR comes back when the series reopens
| series-loads-by-consent | The first slice costs one file and shows at once. Under the guard (256 MB, `image guard <bytes>\|off`) the rest of the stack is then counted in — `LOADING n OF m` on the bar, `READING SLICES n / m · nn%` on the field — and the wheel is honoured only once the stack is whole, because a series that arrives in pieces scrolls in pieces. Over the guard, the stack and every volume layout (MPR, 3D) wait: the bar reads `SERIES <size> · LOAD FOR <STACK\|MPR\|3D>`, a LOAD block appears on the frame, and nothing more is fetched until LOAD, `image load`, or `image --force <path>` says so. A readout that acts says so; a fetch the operator did not ask for is a fetch they cannot stop. | smoke:a volume layout over the guard waits for LOAD and fetches nothing
| a-field-keeps-its-shape | An instrument's field is drawn at the size it has, whatever the frame beside it does: when the mode frame opens or the pane is resized, the engine is resized with its camera kept, so the pixels keep their aspect and every measurement stays on the anatomy it was drawn on. A canvas stretched by the stylesheet is a squeezed image with stranded marks. The probe is a readout, not a mark: it says what is under the pointer while the pointer is there and leaves nothing on the image. | smoke:the field keeps its shape when the frame opens beside it
| a-press-answers-at-once | A click that opens an image is answered before the read is: `image_open` puts the pane on stage at the click, the field says what is being opened (`OPENING <name>`) and the bar says the same, before the series read returns. Counted work says its fraction as a percentage beside the count. A typed `image <path>` is the kernel command instead, and the surface opens the pane from the intent it returns; either way the press yields a pane, and what could not open is named where it would have appeared — never only in the console, the pane never blank. | smoke:image <path> is a kernel command whose intent opens the pane
| a-program-may-take-the-screen | A program may take the whole screen and must give it back: `trek` typed at the transcript console opens the bridge over ARGUS, the page hidden behind it under one body attribute, and ARGUS returns whatever ends the game: a win, a loss, LEAVE BRIDGE (Esc asks first), or a run that threw. The bridge is a player at a terminal, never a rewrite: it runs the session's own `trek` as the operator's command, answers the engine's prompts one at a time from controls lit by the open question, and a dimmed control answers nothing. | smoke:trek takes the screen and gives it back: the page hides behind the bridge, Esc asks, STAY stays, LEAVE ends the game, the console returns READY
| a-program-gives-the-screen-back | The takeover is one attribute on the body, set by the bridge module when it opens and removed in a `finally` around the command, with the stylesheet hiding everything but the bridge under it; nothing else writes it, so no path through the game can leave ARGUS hidden. | lint:a-program-gives-the-screen-back
| a-bridge-answers-the-prompt-in-front-of-it | The bridge never predicts the engine's dialogue: each control answers the question the engine has open, named from its prompt, and a pill whose question is not open presses nothing. What the bridge draws comes from the state the engine shows (`games.trek.state`), never from parsing its scans, so the engine stays the one authority on every rule. | smoke:the bridge answers the engine's own prompts from lit controls, and a dimmed pill presses nothing
| a-pill-that-acts-is-heard-as-the-thing | Every sound is the page's own voice, synthesized from a spectral model; no recording ships. A pill that only opens, sets or answers beeps (one computer beep each); a pill that does something is heard as the thing, and the game's events are heard from what the engine shows and says, so one kill is one explosion. The audio pill is the one mute, and a deployment that keeps recordings of its own still hears them for the frame's beeps. | smoke:the bridge is heard through the page's voice: the bank and every model fetched, none missing, cues played, oscillators started
| a-mark-means-one-thing-everywhere | One symbol table draws every mark the bridge shows. The short-range scan, the course wheels' rims, the target pills and the legend take a kind's symbol from `features/trek/marks.ts`, so a Klingon is the same shape wherever it appears, and the four Klingon marks follow their Klingons by the engine's own lines. The C's glyphs stay in titles, the legend and the log, which is the engine's voice. | lint:a-mark-means-one-thing-everywhere
| a-frame-replays-a-printed-line | Nothing on the bridge moves without an engine line or a change of state behind it, and the frame is the clock: the lines replay in order, each with its cue, the log scrolling with the picture. A torpedo flies the printed track point by point, a Klingon flies its `moves to` line, a kill bursts when its line arrives, a hit beams from the sector the line names and takes the absorbed share off the ring; a move joins the readout before to the readout after along the committed course. A press that acts jumps to the end. | smoke:a frame replays a printed line: every torpedo point, kill, nova, move and hit the picture played is a line in the log, and the track's frames are the printed points
| image-is-a-kernel-command | Opening an image is the kernel's, never the surface's: `image <path>` is a brasa command that resolves a series, a `.dcm`, or a NIfTI/MGZ volume and emits a typed `image.view` intent every surface renders in its own way — ARGUS as a rendered pane, a TTY as the reflection the command prints. The surface claims only the subverbs that drive a pane already on the field (`SHARED_SUBJECTS`) and never shadows the command; help lives in the kernel's own registry. So the same `image ~/uploads/sag-anon` works from a browser and from a terminal on one CALYPSO session. | smoke:a bad image path is refused in the console by the kernel
| a-surface-says-how-to-reach-it | A surface says how to reach the session it is on, from its own address and without asking the session anything: `attach` reads the location the browser arrived by and prints the terminal's line and the browser's URL for the same session. A bearer token is masked in the readout and revealed only when the operator asks for it by name, so a shared screen or a pasted scrollback carries no credential; an address on loopback says that it is, because a second machine cannot reach it. | smoke:the masked form never carries the token, and --reveal does
| a-question-stands-where-it-was-asked | A question the SURFACE puts stands on the pane that provoked it, as a bar across its head: the question stated, the value editable and holding the keyboard, one committing verb reading what it will do, ABANDON beside it, a yes/no as two capsules, and Esc abandoning wherever the hands are. It is exposed as the drawer is, arrives with the press and leaves with its answer, and the console records the exchange so the scrollback keeps the whole session. A question the SESSION puts stays in the console, which exposes itself to carry it: a question in a closed console is a press that reads as dead and a surface waiting on an answer nobody can give. | smoke:a question the surface asks stands on the pane that asked, not in a closed console
| a-component-names-no-theme | A component of the surface and its reference speak the component's own vocabulary — caps, strip, frame, field, track, expanse, level, group — and name no theme. Colour reaches them as tokens, never as LCARS by name, so another theme can sit on the same declaration without a rename; this document is where the theme is named, as the origin of the grammar, and the parts beneath it describe a listing, not a look. | lint:a-component-names-no-theme
| a-share-is-seen-on-the-next-look | A feed shared to this identity is news until it is seen: it leads RUNS marked NEW with who shared it, across logins, until opened or cleared. A feed shared to this identity, unshared, or deleted is seen on the next look at the roster, not on the next full walk: the roster delta holds each collection's total against the last walk's and walks whole when it moved. A look that names a feed the roster lacks — `proc feeds <id>`, `ls /proc/jobs/feed_N`, a RUNS filter that is an id — asks CUBE for it live and either adopts it or says why there is none ("not shared with you, or shared to a group you are not in"); a miss is never silent. The look can be asked for: `proc feeds --fresh`, the roster's REFRESH, walk the index now; `proc jump <n>` and RUNS's JUMP TO go to a feed or node by number, asking which when a number is both; and a walk is never silent either — the bar reads `CHECKING CUBE FOR THE FEED LIST…` while one runs and `FEED LIST CURRENT · n NEW` when it lands. | salsa `procProviderFull` (moved total walks whole; `ls` adopts), brasa `proc-builtin` (`proc feeds <id>` asks; `--fresh` forces the walk), argus `listing` (`filteredEmpty`, `refresh`), smoke runs-filter-asks, runs-refresh
| a-view-is-a-script | Everything a graph shows can be said as a console line that SETS it: `dag layout|projection|draw|scale|hue <word>`, `dag pulse|census|gravity on|off` (bare, they toggle as the pill does), `dag select <feed or job>` (the JUMP TO fly-to) and `dag enter <job>` (then ENTER NODE). A desktop records a graph as those lines — every mode stated, never a toggle, and where it stands last, by `/proc` name so no number is ever asked about — so the script replays the same on any device and for any operator. A shared view starts from the baseline every session starts at, `desktop clear` (the operator's screen kept first as a PANES card, then every pane closed and the dashboard on stage), which also numbers the panes after it as a replay does, so the same lines work loaded, typed or pasted; SHARE VIEW (and `desktop share`) shows that script and copies it, saying COPIED or SELECT + COPY, and COPY LINK puts it in a link (`#view=`, readable). A view received by link or from someone else's file (`desktop load <ChRIS file>`) runs only view lines (`VIEW_LINES_ALLOWED`: view, pane, dag, node, the files pane's showing verbs, `desktop clear`, header, console), shows them and asks RUN or NOT NOW, refuses the whole view when any line asks for more, sets its display modes for the visit only (held in memory, never written to the device's store), and stops at the first line it cannot do, saying why. A files pane records the folder it shows (`file %n open <path>`). | argus `compositions/chris/lang.ts` (`dag_run`), `features/dag/panel.ts` (`dagView_lines`, `view_lines`, `target_jumpTo`), `app/consoleHost.ts` (`desktop_serialize`, `dag_jump`), `app/shareView.ts` (`viewScript_compose`, `shareSheet_open`), `console/argusLang.ts` (`desktop clear|share`, `viewScript_run`, `viewLine_refusal`), `features/feed/view.ts` (`feedModes_hold`)
| a-download-says-how-far-it-has-got | A download stands a transfer row the moment it is pressed, at the foot of the screen whichever pane asked, and keeps it current from the daemon's own count: waiting for CUBE (and how long), bytes of the whole with the rate and the time left, then done, failed with CUBE's reason, or cancelled. CANCEL stops it at the daemon. CUBE is asked once: the probe's stream is held for the browser's save. A session that does not count says the browser has it. | calypso `downloads.ts` (`DownloadLedger`, `/vfs?progress=`, `cancel=`, HEAD `probe=1`), argus `features/files/download.ts`, `features/files/transfer.ts`
| a-link-is-in-sight | A place that can be linked shows its link: the address in full, behind a LINK pill, under the FILES trail and in the RUNS graph's corner (the selected node, else the feed). A press on the pill or the address copies it and the pill says what happened — COPIED, or SELECT + COPY with the address selected for the hand where the browser will not copy — then reads LINK again. A row's LINK points its listing's line at the row and copies it there. Nothing copies silently. | argus `link` (`linkLine_build`, `linkLine_copy`), `features/files/panel.ts` (`link_copy`), `features/dag/panel.ts` (`linkLine_refresh`)
| a-link-lands-where-it-points | A link is the door's address with a path as its fragment, never a session's own address or token. `url` and LINK make one; opening it lands there once — a feed or a node in RUNS, anything else in FILES — and never moves the session's working directory. A feed not shared with the opener says so on the bar, as JUMP TO does. | argus `link` (parse and compose), brasa `url`, the files.row verb contract
| a-question-stands-in-the-frame | Nothing floats over the surface: a question the surface puts to the person (a password, a name typed back, a yes) stands in the frame where the control was pressed, on the takeover's strip, a pane's errand bar or the console's own line, drawn as the engine's questions are. No modal, no sheet, no dialog. | unit:controllerScreen (strip), scriptScreen (errand); smoke:td-controller
| a-controller-leaves-a-script | A controller keyed by a thing (a machine, a cluster) is a way of writing its script, never a way around it: every act it offers is a line of that script, written to the working copy under `/scripts/mine/` before the line runs, so leaving the screen at any step leaves a script that replays to there; a thing the host keeps with no script gets one from its record at the first act. The script screen is the library and the replay; the controller is the door. | unit:vmModel, vmScreen; smoke:vm-controller
| a-running-step-is-followed-not-held | A step that runs long is a task of the session, never a line the screen holds: the seat wears the run `play --status` names (`running`), the screen follows it with the session's own `wait` and the task's log lands under the step, STOP FOLLOWING and LEAVE free the lane as Ctrl-C does and never touch the task, and a script opens on its running seat and follows it again. A hidden word crosses once, on the errand's own bar, hidden and emptied as it is read; the step never shows or keeps it. | unit:scriptModel, scriptScreen, stratusTiles; stratus chrisDeploy; brasa play-builtin
| a-step-is-its-line | A step on the script screen is its lines, and nothing else: one line, or a declaration with the build right after it of the same name (DECLARE and BUILD on one seat, 2026-11-04), or the user lines under USERS; its seat wears the state `play --status` read for those lines from the host's records (not yet, ready, done, changed; a declaration's until it is done, then its build's), never a visit; its body is a form over the lines' own options, each shown as it will run; every control sends a session line the operator could have typed (RUN or DECLARE the line itself, BUILD the declaration then its build, PROTECT and TEARDOWN their own), so nothing on the screen starts a step except by its lines. An edit lives in a working copy under `/scripts/mine/`, written by the editor's own save line; a source's clone is never written from the screen. | unit:scriptModel, scriptScreen; smoke:script-screen
| a-change-is-a-loop | Every control that changes the world is one loop on one panel, (ROC)²: render what stands from its truth with its source and age; offer the change with its options as a difference against what stands; one commit per change to the world, in the offer's section, no half-commit named as one; confirm in place with the consequence; the receipt is the render, read back from the truth; coming back shows where the loop is (<<a-change-is-a-loop>>). Every control a takeover screen makes to send a line carries a `// loop: <id>` mark naming its row in the loop ledger, and every row a mark; a row is held, owed or reads. | lint:a-change-is-a-loop
| a-takeover-keeps-the-frame | A program that takes the screen keeps the ARGUS frame: the gutter at the page's width with its brand block and seat, the head's elbow and the band that closes it, the gap, the body's elbow and band, the views as the page's own seats with LEAVE closing the column, the band's first block and the fillet in the colour of the seat they join, and the page's `main` under the body's band holding every field it draws as a pane under its titled strip. It is built once (`features/takeover/frame.ts`) on argus.css's own rules, where every `takeover-*` class stands beside the page's class in the same selector list, so no takeover measures a frame of its own; what it draws inside the frame is its own. | unit:controllerScreen, trekBridge; smoke:td-controller, trek-bridge; the theme canon
|===

[[a-change-is-a-loop]]
== A change is a loop, seen whole, in one place

Settled with the operator 2026-11-04, after the store's boot disk was moved from 50 to 180 GB on SIZE, RUN pressed, and nothing reached the tenancy while the screen gave no sign either way. The stratus screens had kept every law above and still failed the operator's loop: what is it now, what will this do, did it happen. The operator named the loop ROC, then (ROC)²: RENDER and REPRESENT, OFFER and show OPTIONS, CONFIRM and COMMIT. Its ends are joined: a commit ends in the render.

Every act that changes the world (the tenancy, a CUBE, a record or file another session reads) is one loop on one panel. The operator sees what stands, moves what they would change and sees what the move does, agrees with the consequence in view, and watches the change become what stands, without leaving the panel and without having to ask.

The six clauses, each a check a screen can be held to:

. *Render the truth, and represent where it came from.* The panel shows the value as its truth holds it (for a VM, the tenancy: the instance's shape, the volumes' sizes), with its source and its age. A declared or cached value is never drawn as the current one; where they differ, both are drawn, each named.
. *Offer, and show the options as a difference.* Every control shows, as it moves, its effect against what stands: the difference, the cost, a reboot or downtime, a refusal (a shrink, a new machine). It is worked out here, from what was read once; nothing slow is asked per move.
. *One commit per change to the world.* The control that changes the world stands in the same section as the offer, and nothing on that panel that changes only a record wears a commit's name or place: a half-commit is a trap.
. *Confirm in place, with the consequence.* The question stands on the panel, over the offer's own section, and carries the real consequence (the plan's changes, the cost, what cannot be undone), never a bare yes. It is owed by an act on the world: what builds, bills, destroys or makes an account. A write to a record alone (a declaration, a working copy saved, the clones pulled, a found cluster adopted) and a lock lifted (reversible, and the teardown after it asks) stay one press; their panel says what they write and that nothing builds or bills until the act that asks, and shows their receipt in place (the operator, 2026-11-05).
. *The receipt is the render.* After the commit the same section shows the progress, then the outcome read back from the truth; a failure says what stands now and what to do next. Silence is a failure.
. *Coming back shows the loop.* Leaving and returning finds the same state: running, owed, done or failed.

*Done* means an operator who knows none of the verbs answers three questions from the screen alone: what is it now, what will this do, did it happen. An operator who has to ask "did it take?" means the slice failed, whatever its tests say.

[[a-control-offers-only-what-can-be-done]]
=== A control offers only what can be done

Settled with the operator 2026-11-07, after SPEC let the store's boot disk be set from 150 to 50 GB (a boot volume only grows) beside a valid data change, then shut the whole BUILD with "BOOT DISK 150 → 50 GB IS REFUSED" and a note under the dead button still promising a password and a plan: the valid change could not be built, and nothing said so before the hand moved.

What the screen knows is impossible is never offered. A control's range is the legal set: a slider's and its number box's floor on a machine that stands is what runs for a disk that only grows; memory moves within its shape's bounds per vCPU; a field the engine would refuse to change on a machine that stands (its OS) is fixed while it stands, and says why; a kind, a port or a name that cannot be added is not offered; a profile not built yet, or that the machine is too small for, is shown and is not a choice; a group CUBE does not hold is never offered. A value typed out of range is marked where it is typed and never becomes a change. What only the engine can refuse, and the screen learns only from a plan or a declaration made elsewhere, is listed with its reason among the changes, left out, and the act carries the rest: no control is ever a dead button that blocks what can be done. A button that cannot be pressed is not drawn (the operator, 2026-11-07): BUILD, APPLY, ADD and UNDO stand only while there is something they can do and the session can do it; until then what is still needed is said in their place ("to add: a port"), and a READ session that cannot be raised is told why instead of shown a refused button; a fixed state (every account in `all_users`) is shown as a chip, never a disabled button. The words under a control say what pressing it does (the password first in READ, the plan, the question), and stand only while it can be pressed.

[[design-loop]]
=== The design loop

A surface slice that adds or changes a control that acts goes round this loop, in this order, and a pull request shows each turn:

. *The journey first* (journeys-before-verbs): the operator's words for what they want, and the panel it happens on.
. *The loop row before code:* the control's row in the ledger below, naming what Render reads and from where, the difference Options shows, the consequence Confirm carries and the receipt Commit leaves, and the clauses it holds or owes. No row, no code: `lint:a-change-is-a-loop` refuses an acting control without one.
. *Build* the control over its row.
. *Walk the loop* on the entry path the operator uses (porter, then the session): Render, Offer and Confirm over the real tenancy, read-only; Commit over `stratus --fake`, or over the real tenancy with the operator's word.
. *Show it:* a screenshot of each beat (what stands, the offer moved, the question, the receipt), sent to the operator, and the three questions answered from them.
. *Hold it:* the row's standing moves to `held`, naming the smoke scenario that walks it, once that scenario exists and passes; until then the row says which clauses it owes and why, and the owed clauses are the next slice's work.

[[loop-ledger]]
=== The loop ledger

One row per control that sends a line from a takeover screen (the TD controller, the VM controller, the script screen), keyed by the `// loop: <id>` mark at the line that makes it. Standing is `held` (every clause, with the smoke scenario that proves it), `owes` (the clauses not yet held, and why), or `reads` (the control changes nothing; the loop's clauses do not apply). The ChRIS panes' acts (a feed deleted, a plugin run, an upload) enter the ledger as each is next touched. First audit 2026-11-04, from the code and the walk of #1376; a row is corrected when its loop is walked. The VM controller's rows (and the script screen's CHRIS DEPLOY and USERS forms it shows) were walked the same night over the real tenancy, read-only, from a session with no records of its own, and their commits over the fake cloud.

[cols="2,2,2,3,2,5"]
|===
| Loop | Screen | Control | Sends | Changes | Standing

| vm-size-build | VM controller · SIZE | BUILD | `vm new <vm> …`, then `vm build <vm>` | the tenancy | held: smoke:what runs beside every field of SIZE is read from the tenancy, the ports, the backups and the OS too
| vm-services-build | VM controller · SERVICES | BUILD | `vm new <vm> --open … [--ssh <port>\|off --ssh-user … --ssh-key …]`, then `vm build <vm>` | the tenancy and the machine | owes 6: walked 2026-11-07 on the fake and over the real store to the question (answered no, nothing opened). The rows read what the line opens beside what the tenancy opens, the web's ports with what answers there read live; the change and BUILD name what opens and closes; the build asks with SSH's warning, opens the ports, and its host step (SET UP) admits them in the machine's firewall and sets public SSH up over the bastion, the receipt the machine's own setup read back on the row. Owed: a leave mid-run walked on this seat
| vm-new-commit | VM controller · A NEW MACHINE | BUILD | `touch … /scripts/mine/<vm>.mise`, `vm new <vm> …`, then `vm build <vm>` | a file, the record, and the tenancy | held: smoke:vm on a new machine offers its levers and its price before anything is sent; BUILD, its one commit, writes the script, declares and builds, its question in place with the cost
| vm-protect | VM controller · TEARDOWN | PROTECT · ON, PROTECT · OFF | `vm protect <vm> on\|off` | its record | held: smoke:the lock is kept in the tenancy: THE GUARD says where it is kept and when it was set, PROTECT · OFF lifts it there and the screen reads it back
| vm-teardown | VM controller · TEARDOWN | TEARDOWN | `vm teardown <vm>` | the tenancy | held: smoke:what runs beside every field of SIZE is read from the tenancy, the ports, the backups and the OS too
| vm-teardown-all | VM controller · TEARDOWN | TEARDOWN · ALL | `vm teardown <vm> --all` | the tenancy | owes 1: walked 2026-11-04 as TEARDOWN; the name typed back stands in place
| ss-pull | script screen · LIBRARY | PULL | `scripts pull` | this host's clones | owes 5: the clones' heads before and after are not shown
| ss-save | script screen · a step | SAVE | `touch --withContents=… <copy>` | a file | owes 5: the working copy written is said only by the chip Leaving with edits not saved keeps them: the console says how many were kept, and the script opened again lays them over its lines, said as UNSAVED EDITS KEPT with SAVE beside them; never dropped silently (round 10, clause 6d).
| ss-net-declare | script screen · NET NEW | DECLARE | `net new <net> …` | its record | owes 5: walked 2026-11-05 over the fake cloud. Clauses 1 to 4 hold: the CIDR as it stands is the VCN's, read from the tenancy with when (`stands` on `net status`'s row), and a new CIDR on a built network says, as it is declared, that it replaces the VCN and subnet and every machine on them; not yet held by a smoke stage
| ss-net-build | script screen · NET NEW, NET BUILD | BUILD, RUN | `net new <net> …` then `net build <net>`; `net build <net>` | the tenancy | owes 5: walked 2026-11-05 over the fake cloud, BUILD to its receipt: THE NETWORK · AS IT STANDS drew BUILT, the CIDR as the VCN holds it read back after the apply, its stack ACTIVE and its bastion with the address it admits, with when. Every clause held on the walk; not yet held by a smoke stage The fake tenancy lists a network's stack from the plan that made it, as OCI does, so the seat read while BUILD asks draws the stack the question names, not "none found" (round 10, clause 1b).
| ss-net-teardown | script screen · NET NEW, NET BUILD | TEARDOWN | `net teardown <net>` | the tenancy | owes 5: walked 2026-11-05 over the fake cloud, TEARDOWN to its receipt: the name typed back in place, then GONE, no stack found in the tenancy, and the CIDR said as none standing (the range it was built with as history). Every clause held on the walk; not yet held by a smoke stage
| ss-vm-declare | script screen · VM NEW | DECLARE | `vm new <vm> …` | its record | owes 1, 2: as SIZE; on the script screen declaring is authoring, so clause 3 holds here; at READ DECLARE raises first, since the tenancy keeps a VM's declaration
| ss-vm-build | script screen · VM NEW, VM BUILD | BUILD, RUN | `vm new <vm> …` then `vm build <vm>`; `vm build <vm>` | the tenancy | owes 1, 2, 5: as SIZE's BUILD A yes answered on the strip for a line this screen did not send (the machine's own question, asked after a leave and return), here and on the VM controller, reads RUNNING, its commits shut, until the standing names its task, which the screen then follows; a standing read before the daemon starts the task never draws the line idle (round 10, from the frozen grader: RUNNING LINE nothing, ▸ READY and BUILD live after the yes).
| ss-vm-protect | script screen · VM NEW, VM BUILD | PROTECT · ON, PROTECT · OFF | `vm protect <vm> on\|off` | its record | owes 1: the lock is not drawn on this seat, only offered; at READ it raises first, as on TEARDOWN
| ss-vm-teardown | script screen · VM NEW, VM BUILD | TEARDOWN, TEARDOWN · ALL | `vm teardown <vm> [--all]` | the tenancy | owes 1, 5: no blocks here, only the step's output Leaving with its question standing carries the question, unanswered, to the screen's keeper; it stands on the strip again when the screen opens (bounded at 30 minutes, then abandoned). The VM controller does the same.
| ss-chris-deploy | script screen · CHRIS DEPLOY; VM controller · CHRIS | RUN | `chris deploy <vm> …` | the VM and its CUBE | owes 1: walked 2026-11-05 on the VM controller's CHRIS seat over the real store, a replay deploy on the operator's word. Clauses 2 to 6 hold: DEPLOY WOULD drew the plan, the replay asked in place with it (restarts CUBE and porter, perhaps; no session open), and the receipt was what runs read back (CUBE 200, chris signs in, porter 200, ghcr.io/fnndsc/cube:6.11.0 and porter 0.9.11, deployed 14:51; WHAT RUNS IS WHAT THIS DEPLOYS). Owed: the image and porter version are the deploy's record, not read from the machine A first deploy asks in place before anything is put up: what goes up (CUBE with its workers, Postgres, dragonfly, nats and pfcon, the compute, the names), the admin password asked after the yes; the seat draws no ChRIS on the machine, with when chris status read it, and DEPLOY WOULD from --plan before RUN (round 10, from the frozen grader: a bare password field, nothing read before RUN).
| ss-user-add | script screen · USERS; VM controller · USERS | ADD, RUN | `chris user add <name> …` | the CUBE | owes 5: walked 2026-11-05 on the VM controller's USERS seat over the real store, the account mise-walk added on the operator's word: the receipt holds (mise-walk appears in CUBE's accounts, read from it, and the line's seat reads done once CUBE holds it). The walk found ADD's line on a seat with no lines yet left `--on` out; it names the machine on screen now. Not yet held by a smoke stage (the fake runs no CUBE) On the VM controller ADD is now APPLY on ITS USERS (2026-11-07): a new user's line is saved into the working copy first, then each pending change runs in turn (`chris user add … --groups`, `chris user mod`, `chris passwd`), the engine asking each; walked on the fake to YES with a user added into pacs_users. ADD saves the working copy with the new line, then runs it; the engine asks first, on the strip, naming the account, its address and the ChRIS, with CUBE's accounts read before the question: an account already there is left as it is, unasked, and RUN on a user line asks the same (round 10, from the frozen grader: RUN made an account unasked). ADD reads NO CHRIS, and says where to deploy one, when chris status (or, on the VM controller, vm status) reads none where the account would go, before the press; an account it makes is always declared by a saved line; a save refused runs nothing.
| vm-user-passwd | VM controller · CHRIS · ITS USERS, a row's editor | SET PASSWORD · <name>; for `chris`, YES · SET AND RESTART CUBE in the row | `chris passwd <name> --on <vm>` | the CUBE (for `chris`: this host, `cube.env`, CUBE's restart, the tenancy) | owes 5: walked 2026-11-07 over the fake cloud, an ordinary account's and the admin's. The press is the confirm for an ordinary account (reversible, bills nothing, makes no account: clause 4a); the admin's restarts CUBE, so the row asks once, in place, and its YES answers the engine's question. The password, typed twice and matched as typed (or GENERATE, shown once), answers the engine's two hidden questions from the row: never a command line, the log or the strip, and gone from the page once sent. The receipt is the row's: password set · HH:MM. Not yet held by a smoke stage
| ss-line-run | script screen · a line with no form | RUN | the line as written | what the line does | owes 1, 2, 4, 5: a line the screen has no form for has no render; it runs as typed. `chris plugin sync` is such a line: RUN sends it, its question (the plan's counts) stands on the strip, and its task's log follows; a seat of its own, with the plan drawn as the offer, comes after the design pass
| td-plan | TD controller · BUILD, SIZE | PLAN | `cluster plan <name>` | a stack's plan in the tenancy | owes 1: walked 2026-11-05 over the fake cloud (the real tenancy runs no cluster now); the plan is drawn with when it was made (THE PLAN MADE …), but what the tenancy runs is not drawn against it
| td-verify | TD controller · BUILD, VERIFY | VERIFY | `cluster verify <name>` | nothing | reads
| td-spend | TD controller · BUILD | SPEND | `spend <name>` | nothing | reads
| td-build | TD controller · BUILD | BUILD, BUILD · THROUGH RUN | `cluster build <name>` | the tenancy | owes 1: walked 2026-11-05 over the fake cloud. Clauses 2, 4 and 6 hold: the plan's rows stand beside BUILD, the billing question prices what SIZE priced, a running build is followed; each step that makes something in OCI (the ISO's bucket, the machines and balancers, the DNS zone) is drawn against what the compartment holds as counted, with when, and a step done of which the tenancy holds none is drawn as the disagreement. Owed: the steps that make nothing in OCI (the preflight, the records at Red Hat, the install) are the record's A resume past the billing step asks first (round 9, from the fresh grader: BUILD · THROUGH RUN after KILL built on to chris-seed unasked): the steps left, the rate what stands bills on at, what a step stopped part way may have left running in the cloud, and the test user chris-seed makes; a NO changes nothing.
| td-follow | TD controller · a running step | FOLLOW | `cluster build <name>` (a build) or `wait <job>` (a teardown) | nothing: it follows | reads
| td-kill | TD controller · a running step | KILL | `kill <job>` | the running task | owes 1: walked 2026-11-05 over the fake cloud. Clause 4 holds: the first press arms KILL and says in place what stopping part way leaves (what it made stays and bills; BUILD or TEAR DOWN resumes at the first step not done), KEEP IT RUNNING disarms it, KILL · NOW sends. Owed: what the step has made so far is not drawn beside it
| td-new-commit | TD controller · NEW · SIZE | BUILD | `cluster new <name> …`, then `play ~/tenant.mise --param NAME=<name>` | its record | owes 1: walked 2026-11-05 over the fake cloud. Clauses 2 to 6 hold: the levers, the fit and the price stand before anything is sent; BUILD, its one commit, writes the spec and plays the tenant, whose build asks first, in place, everything it makes before it bills (the compartment, the cluster and its infra-env at Red Hat, the discovery ISO and its public link, the plan) at the rate SIZE priced, and a NO makes nothing; billing is asked again with the plan's count and the same rate. Owed: the zones' capacity reads NOT SAID where the cloud does not answer
| td-adopt | TD controller · a found cluster | ADOPT | `cluster adopt <name>` | its record | owes 5: not walked (the tenancy and the fake run no found cluster); what adopting took over is said in the log, not drawn
| td-lock | TD controller · TEARDOWN | LIFT THE LOCK, LOCK AGAIN | `cluster protect <name> off\|on` | its record | owes 5: the lock is the tenancy's (the vault's `mise-cluster-<name>`), read on each status and again before a teardown; the guard says where it is kept and when it was set, and at READ the lock's pills raise first; held in unit tests, not yet walked (the smoke builds no cluster)
| td-teardown | TD controller · TEARDOWN | TEAR DOWN, REMOVE WHAT IS LEFT | `cluster teardown <name>` | the tenancy | owes 1: walked 2026-11-05 over the fake cloud. Clauses 2, 4, 5 and 6 hold: the guard says what a teardown stops billing beside its pills, the name is typed back, the blocks are the receipt, a teardown under way is followed. Owed: STANDS is the record's, its age unsaid Before a teardown, what no count read is NOT COUNTED (the line counts it as it runs), never STANDS; torn down, the build's count reads WAS n OF n DONE · ALL GONE and LAST DONE says before the teardown (round 10, clauses 1b and 1c).
| confirm | any takeover screen · the question | CONTINUE, YES, SEND, DESTROY, STOP, NO | the answer to the engine's question | the answer | reads: the Confirm of the loop it answers, held by that loop's row
|===

*What no row owns.* The walk of 2026-11-04 found two failures under every VM row, in the kernel; both were closed on 2026-11-05 (docs/stratus.adoc, D16 revised): a MANAGE session's declaration is kept in the tenancy and read by every session (a READ session's stays on its host and says so), and each listing reads what runs (vCPUs, memory, the boot and data volumes' sizes, with when). What remains under clause 1 for every VM row: the open ports, the backups and the OS are still read from the declaration, not the cloud.

== LCARS: what it is, and why ARGUS started there

LCARS — the *Library Computer Access and Retrieval System* — is the fictional computer interface of the Starfleet universe, designed by scenic artist Michael Okuda for _Star Trek: The Next Generation_ (1987) and elaborated across the franchise since. Its look is unmistakable: flat rectilinear panels in warm color families, swept "elbow" junctions, pill-shaped buttons, condensed uppercase type, and dense numeric captions — all butted edge to edge with no overlap, no window chrome, no cursor-centric idiom at all. The name and the visual designs are the property of CBS Studios / Paramount; this document uses "LCARS" only as a historical and technical reference to that body of work (see the precedents section for sources).

ARGUS started from LCARS for three honest reasons. First, fit: a live operator console for medical compute wants exactly what the fiction depicts — glanceable state, fixed geometry, controls that read as regions of a single instrument rather than floating windows; the structural argument is made in link:../../../docs/workspace-model.adoc[the workspace model]. Second, discipline: beneath the costume sits a real design discipline (no overlap, color as role, frame as control) that certified operator interfaces — cockpits, radiology workstations — arrived at independently, so building against it is building against proven practice. Third, candidly, resonance: the aesthetic communicates instantly what kind of instrument this wants to be. The early experiments validated the fit — a consistent UI language formed faster under LCARS constraints than the previous ChRIS UI ever achieved without them.

== Why AEGIS exists: LCARS taken seriously

Nobody has done this. Decades of LCARS artifacts exist — launchers, skins, dashboards, templates — and all of them are cosmetic: they repaint chrome while nothing underneath obeys the language. The fiction shipped an appearance, never an interaction model, so every imitator cloned the only thing there was to clone: the paint. The absence of a serious attempt is therefore evidence of nothing; there is no failed attempt on record.

Meanwhile the *discipline* underneath LCARS is already proven under other names. Fixed rectilinear partitions, no overlap, color as system role, glanceable state under operator load — that is avionics (ECAM color semantics are law), that is the radiology workstation, that is tmux culture. ARGUS joins two proven halves that were never joined: LCARS-the-costume and LCARS-the-discipline, with live PACS traffic and execution graphs underneath — the real workload every fan project lacked.

The prop DNA carries real hazards, and policing them is AEGIS's job. Okuda designed for camera distance: glanceability over density, ornament tolerated, contrast questionable, uppercase everywhere. Where the costume fights ergonomics, AEGIS sides with the operator — and says so explicitly, rule by rule, rather than inheriting the prop by mood.

== What ARGUS is, in desktop terms

ARGUS is an unnamed tiling window manager wearing load-bearing decoration. In real tiling WMs, decoration is what got exiled — the frame shrinks to a one-pixel border, negative space. LCARS inverts this: the frame is the control surface. Capsules are verbs, the gutter is a preset switcher, elbows declare structure. The titlebar never died here; it got promoted.

The second difference matters more: jurisdiction. A window manager manages *foreign* windows and its rules stop at the border. ARGUS panes are native projections of one envelope-model family, so AEGIS rules reach inside the pane — anatomy, palette roles, claim behaviour, linkage. ARGUS is closer to acme or an avionics console than to a desktop: not a manager of applications, but a single instrument with subdivisions.

== The first principle: machinery behind the frame

*At rest, a pane shows only work and state. Machinery lives behind the frame; the frame is the handle.*

"No chrome" is the wrong reading of LCARS — the vow is no *dead* chrome and no *floating* chrome; frame-as-control is canonical. But always-visible generic meta-verbs (split, zoom, close) on every pane header multiply with every pane: six panes, six identical capsule clusters. That is noise scaling linearly with success, and in-fiction consoles show no window-management buttons at all.

The resolution is the *pane drawer*: every pane carries a top LCARS border; a gesture on that border (click) glides open a hidden control surface holding the pane's machinery — layout verbs, semantic children, pane-local controls. This is the mouse-native prefix key: tmux shows zero chrome because its verbs live behind a modal gesture, and the drawer is the same structure, pointer-shaped. ARGUS already owns the motif (the console lid), and the fiction backs it — LCARS panels slide away to reveal the machinery behind.

Corollaries:

* Every pane carries the top border. The border is a wide target (Fitts-friendly) and doubles as the pane's title bar.
* Zoom and close migrate into the drawer with everything else, or the principle is hypocrisy on day one.
* Jurisdiction split: the left gutter is *global* reconfiguration (presets, "reset to givens"); the drawer is *per-pane* machinery. The two never trade verbs.
* Every drawer verb has a command-line twin (`layout ...`), under the settled symmetry: mouse path and text path converge on the same intents.
* The keyboard path is tmux-shaped: Ctrl-B is the prefix, opening the focused pane's drawer with keyboard focus on its first verb (Tab walks, Enter fires); with the drawer open one key presses one capsule — the chords (`argus keys`): tmux's `%` `"` `z` `x` `o` `:`, vim's `h j k l` to split, `H J K L` to move, `m` to arm MOVE, `u f v` the binding, `1 2 3` the claim, the arrows focusing the pane in that direction; Esc closes any open drawer before anything else claims the key. The drawer being one surface for mouse and keyboard is the point — a chord is a key on a capsule, never a verb of its own, and each capsule's title names its key.

Costs, accepted with eyes open: hidden machinery is invisible until taught — the first session must teach the gesture (a subtle border affordance, and the operator companion). The extra click on layout verbs is the correct trade: split is rare relative to work, and optimizing rare verbs for invisibility beats optimizing them for speed.

*Status:* the drawer shipped with the first slice — stamped panes (files, DAG, viewer, empty) carry their machinery behind the frame, header capsules gone. Remaining violation: the static PACS workspace pane keeps its old chrome until it joins the instance model.

== Semantic children: what goes into a new pane

The workspace model settled the universal answer: the pane specifier is an intent — a split yields an empty pane whose prompt claims it. The drawer completes that design rather than overthrowing it: alongside the raw split verbs, a pane's drawer offers *semantic children* — parent-contextualized, pre-composed intents.

* A file browser's drawer offers "browser here" — literally `ls <this pane's root>`, pre-filled.
* It offers "content viewer" — a view intent slaved to the parent's selection, showing whatever file the parent regards.
* A DAG pane's drawer might offer "browser into selected node" — the node's `vfsPath`, pre-filled.

These are shortcuts, not a chooser: they feed the same claim pipeline the empty-pane prompt feeds, and the empty pane remains the universal fallback item in every drawer. Because the offers derive from the parent's kind and state, the list grows with envelope models — it never becomes the hand-curated menu that scales badly.

== Pane linkage: subjects, not edges

The terminal and the file browser were linked "by accident" — and the accident is instructive: they are not linked to each other at all. Both observe the same shared truth, the session cwd. No pane-to-pane edge exists, which is exactly why it works. The radiology precedent has the same skeleton: MPR viewers (axial/sagittal/coronal) never message each other — all three observe one crosshair position in volume space. Nobody is master; the *subject* is.

AEGIS elevates the accident to law:

*Panes link by subscribing to named subjects — hub-and-spoke, never pane-to-pane.*

* *Direction for free:* slave = reads the subject; master = writes it; bidirectional = both write. Uni- versus bi-directional linkage is just read versus read/write membership.
* *Scope is membership, not ancestry.* Link groups are an overlay orthogonal to the layout tree — the tree stays pure geometry (radiology sync groups, exactly). Coupling links to tree topology would be a category error. One default is right, though: a child spawned from a parent's drawer inherits the parent's subjects — that is the semantic tracing the drawer promises.
* *Lifecycle dissolves.* A master closing leaves the subject standing; slaves hold the last value. No orphan-edge cleanup, no ownership fight.

=== Where subjects live

The workspace-model doctrine — *brasa states facts and relevance; surfaces decide geometry* — rules a third time without amendment:

[cols="1,2,1"]
|===
| Class | Examples | Home

| Subject vocabulary
| What a subject is: naming, value shape, read/write membership.
| `menu` (schema tier, beside the LayoutNode algebra)

| Session-truth subjects
| cwd, current feed, active PACS query, *regard* (below).
| `brasa` — the prompt-context push *is* this mechanism already

| View-state subjects
| MPR crosshair, scroll positions, hover.
| argus-local bus — view physics never crosses the wire
|===

=== The addressability test (locked)

The line between session truth and view state is principled, not case-by-case:

*If a subject's value is a VFS address (or a model reference), it is session truth and belongs to brasa. If it is continuous view-space coordinates, it is view state and stays in the surface.*

This matches the doctrine verbatim: the kernel speaks names in the namespace, never pixels. A selected file has an address — brasa. A selected DAG node has an address (`vfsPath` is on the model) — brasa. A crosshair has voxel coordinates in a rendering — argus. Frequency agrees: address-valued subjects change at human click-rate; coordinate-valued subjects change at drag-rate.

=== Regard: what the operator indicates (locked)

Selection is session truth masquerading as view state, and the precedents are decisive: X11 primary selection lives in the *server* so any client can consume it without the pointing client knowing who listens; Plan 9's plumber makes "the thing the user indicated" system truth, and whole ecosystems exist only because it was promoted from day one. brasa is the server analog.

The skeletal foundation, deliberately small: one session subject in brasa named *regard* — the thing the operator currently indicates — valued as a VFS address plus model kind, riding the same push channel as prompt context, with its schema in `menu`. The slaved content viewer subscribes to regard; the terminal's `cd` already writes the sibling subject (cwd). One subject, one schema, an existing channel.

What it buys immediately: the accidental terminal↔browser link and the deliberate viewer link become the same mechanism, and any future assist layer can ask the kernel "what is the operator looking at."

=== Regard, grilled: the two-layer model (locked)

Scope is neither purely global (X11's one PRIMARY) nor purely scoped (radiology sync groups) — it is both layers, from a single write path:

* *Group layer — where things render.* Every indicating pane sits in a link group (possibly of one). A regard write lands in the writer's group. One rendering rule: the group's regard renders in the group's viewer member; absent a viewer, the writing pane renders it inline as an overlay (exit restores the pane). The lone browser overlaying its own content and the browser routing to a spawned viewer are the same mechanism — membership differs, nothing else.
* *Session layer — what the operator regards.* Every regard write, whatever its group, also flows to brasa tagged `{address, modelKind, groupId, paneId}`; the session's current regard is the latest write with provenance attached. Not a second mechanism — the same write, observed at session level. The kernel's one-answer question stays answerable without disturbing any group's viewer.

Write semantics: a subject is a *retained cell, not an event stream* (MQTT retained-message semantics). Last write wins — a "conflict" between two writers in one group is just the operator moving their attention; the provenance tag says who wrote. A late joiner immediately receives the retained value, so spawn-then-see workflows feel alive. "No value yet" is a schema-legal state rendered as a defined LCARS awaiting-state, not an error.

Lifecycle: a group dies when its last member leaves; nothing is retained. Session regard lives in brasa and survives a surface reload. Groups and carved trees do not survive reload — session-only, consistent with the carved-tree rule.

=== Watch: the pane is the subscription (locked 2026-09-02)

Regard says what the operator indicates. It does not say what a surface is *keeping open* — and those are different nouns with different lifetimes: regard is one retained cell for the session, last write wins; an open DAG pane on a running feed is an interest that lasts exactly as long as the pane, may coexist with another pane's interest in another feed, and must die with the surface that held it. Overloading regard for this breaks the moment two panes look at two feeds, or the operator regards a file while a pipeline runs. So liveness has its own subject: a *watch*.

The kernel never polls CUBE on its own — that stays law. (It may read its own local disk: the calypso daemon fingerprints its own code once a minute to know whether it is stale, #863.) The open pane is the subscription: on opening a feed it sends `watch` with the feed's address, on leaving it sends `unwatch`, and a detached surface releases every watch it held. While anyone watches a feed the engine samples it — one feed-row fetch, and only when the counters moved a delta of what changed — on an adaptive cadence (three seconds while the feed changes, doubling on quiet ticks to thirty), publishing the refreshed `feed.dag` model to every surface as a session-bus envelope from the `daemon` surface, off the scrollback, because it is state rather than something the operator did. A feed that settles ends its own watch: there is nothing left to keep live. The `watched` message reports the honest state — `live`, `settled`, or `stale` when the last sample failed — and the pane's bar shows it (honest-wait: an observer must know whether what they see is current). The console form is `proc watch <feed>` / `proc unwatch <feed>`; the wire pair and the verb share one implementation, so chell can ask for the same liveness argus gets.

Payload discipline: the wire carries the whole collapsed model every time; the pane diffs it against what it holds by signature-stable group id, so a tick and a manual REFRESH are the same code path and the census only re-expands a group whose count changed.

=== Rooted versus slaved children

A DAG node click showcases the composition: the click writes group regard; the drawer's semantic children are parameterized by pane kind *and current regard value* — file view of this node, runtime inspector (execution state and progression), log view, note editor. Two distinct bindings, both from the same drawer:

* *Rooted child:* consumes the regard value at spawn time, then frozen — a browser rooted at the node's data directory stays there as other nodes are clicked. Not a group member; an intent pre-composed with a snapshot.
* *Slaved child:* subscribes to group regard and follows — click across nodes and the runtime and log panes retarget live. The MPR moment for compute.

The retained cell makes both feel right: a log pane spawned after the click shows that node's logs instantly.

=== The pulse wave and the fly-in (grilled, locked)

Two DAG behaviors, both settled by grilling on 2026-08-30.

*The pulse wave* animates the graph in dependency order: a node flares when every parent — join parents included — has flared, so a topological join visibly waits for its last input. The wave is history-honest: only nodes that actually executed (terminal success or error) fire; cancelled and still-pending nodes block their subtrees, so on a running feed the wave washes up to exactly the execution frontier and stops there. True timestamp replay (and pulse spacing that encodes wall time) upgrades this when the proc-cache metric slice lands; the dependency wave needs no new data. Trigger discipline: one wave on graph load, then the rail's PULSE pill on demand — the reference pane is a status instrument, not a screensaver. The ambient miniature is the opposite case and pulses always.

*The fly-in* makes activation literal navigation: dblclick dollies the camera into the node, and on arrival a rooted browser on the node's data directory overlays the same pane — the scene pauses underneath, never torn down, so Esc reverses the dolly and the DAG is back. One Esc press exits the node (directory back-walking is the drawer's BACK verb; Esc never depends on invisible browser depth). The overlay is the DAG pane transformed, not a new pane: it keeps the pane's identity and group, so file clicks inside the node write the pane's regard and any slaved viewer follows. The session-global gesture survives in the drawer as ENTER NODE — the old visible `cd` into the node — keeping fly-in view-local and side-effect free.

=== Contextual back (grilled, locked 2026-08-31)

Esc is a contextual back in the Android sense, not a fixed shortcut: each pane owns a navigation stack, entry gestures push levels, and Esc pops exactly one. The DAG pane's stack reads `feed list → graph → node immersion → file view`; fully inside a file, successive presses walk all the way back out to the list. A file view inside a node is a level because it is visible (settled 2026-09-02, correcting the 2026-08-30 one-press exit); directory depth inside the node is not visible and stays BACK's job — Esc never walks invisible depth. Transient chrome pops before navigation — an open drawer first, then a zoom — because chrome is view state, not a place the operator went. Selecting a feed in the list ENTERS it: the full graph takes the pane, the list steps aside, Esc returns (settled 2026-08-31, correcting an earlier half-pane "preview above the list" that split the pane and satisfied nobody). Live scrubbing — the list updating a graph as the selection moves — returns only when a context split exists to receive it, per the original design walk.

The gutter preset stays a declaration: RUNS-02 always lands on the pure feed roster, dismissing any graph a previous visit left up — no ghost renders. Every gesture in this stack funnels through a named intent (`list_reset`, `nav_pop`, enter, immerse), deliberately shaped so a text command layer — a navigation macro language driving the same intents from the embedded terminal, for testing, executable help, and replay — can bolt on without touching the gesture paths. That layer is designed but not yet grilled.

=== The desktop contract (locked 2026-08-31)

The workspace is a desktop, not a document. Before this was law, the layout tree had only a width contract: panes sat in normal page flow, every scrollable interior was capped by a viewport guess (`42vh` on the DAG canvas, the feed roster, the file listing; `calc(100dvh - 22rem)` on the viewer body), and a "full-workspace preset" was full-width but content-tall — a document cosplaying as a desktop, which the operator read correctly as clipping.

The contract: the shell owns the viewport (`100dvh`, the page never scrolls); the header takes what it needs, the console drawer its declared height, and the main region flexes into the rest; every leaf fills, every pane is a flex column, and scrolling happens inside instruments — the roster, the listing, the viewer body — never on the page. And when the fixed pieces outgrow the viewport, they YIELD in a fixed order: the console drawer shrinks before the workspace does — the workspace keeps a floor (`main { min-height: 20rem }`) because panes are the point of the page. Freezing the drawer (`flex: none`) instead crushed the workspace to a sliver on any screen where header plus console exceeded the viewport — the first regression the contract shipped. Fixed viewport fractions in pane interiors are banned: an interior is either chrome (auto-sized) or work surface (flex-fill with its own scroll). The zoom special-cases (`calc` heights for the zoomed canvas) died with it; a definite-height flex chain needs no arithmetic. Precedent: this is the acme/tiling-WM discipline the ARGUS framing already claimed — the instrument has total jurisdiction over its panes, and a pane's size comes from the composition, never from its content.

=== The daemon console (settled 2026-09-09; supersedes the console face)

The daemon's terminal is a surface too, and AEGIS reaches it. The shape settled on 2026-08-31 was a two-screen split: the classic in-scroll boot, then a resting *face* on the alternate buffer (the animating brain, one status line, the boot log behind Esc). That face is gone. It was a screensaver where a session belonged: the call-out is a Linux boot, and a Linux boot ends at a login, not at a picture.

*Booting* is unchanged: the brain prints, its lights come alive when credentials clear, and the boot log scrolls beneath it with every step tagged — including the steps that warm behind the prompt, which now settle on this readout as `[ OK ]` with what they cached (the daemon keeps its log, so an outcome has somewhere to land) rather than leaving a `[PENDING]` row open forever.

*Ready* is a login. The identity was resolved before the warm-up began, so there is nothing to ask; the login is the attach. Once the daemon is listening it puts an ordinary `chell --remote --attach` surface on its own terminal, and the boot log simply continues into a prompt. Three rules make it honest:

* *It is a surface like any other.* A child process on the wire, one loopback hop from the daemon, exactly what every other surface pays. Not an in-process shortcut: the daemon holds one sink and one interaction surface for all its sessions, and a REPL started inside it would take those over and silence every other surface. The console claims nothing the wire does not give a remote chell.
* *`exit` detaches, never stops.* The daemon stays up with its terminal idle; Enter attaches again; Ctrl-C at the idle terminal stops the daemon as it always did. While a surface holds the terminal the surface's readline owns Ctrl-C (it clears the typed line, as chell always has).
* *The daemon's own words wait their turn.* Whatever the daemon writes while the console has the terminal — a roster row, a topology row, a late warm-up — is held and handed back on detach, after the surface's last line, under `While the console was attached:`. Nothing lands across a prompt. Inside a surface the same discipline holds for what the daemon pushes to it: a stray line clears the idle prompt line, lands on its own line, and the prompt is redrawn beneath it with the typed text intact (chell's prompt guard, which every remote surface now has).

Off a TTY (systemd, nohup) nothing attaches and the log stays sequential, as before. The standalone `calypso` binary still raises the old face; it has no chell to put on its terminal, and is the next thing to fold in.

=== The bar law (locked 2026-09-01)

Sizing and layout machinery never rides a title bar. A bar is three things and only three: *identity* (the title), *state* (readouts like MODE — annunciation, not actuation), and *the handle* (the whole bar opens the drawer, where the machinery lives). This is machinery-behind-the-frame applied without exception: the console, which had accreted a zoom square and a close pill on its bar, reaches parity through its own locked idiom — the lid toggles, the drag strip resizes, and the language (`console zoom`, `console height <px>`) carries the rest. The LANG capsule lives on the lid strip: a machinery control on a machinery row.

End-caps are punctuation. In Okuda's grammar the rounded cap terminates a bar; it is frame, not control. Ours are passive and frame-colored. Red appears on a bar only when something is actually wrong — the flight-deck color law spent forward: a standing red half-pill on every pane was a standing false alarm, and the dark cockpit annunciates nothing at rest.

=== The drawer grammar (grilled, locked 2026-08-31)

The behind-pane drawer is a sentence, not a menu: *shape the tile, bind a neighbor, act on the content*. Three groups, three colors, identical on every pane at every depth — a newly spawned pane exposes exactly the same drawer, so composition nests indefinitely under one grammar.

*Structure* (harvestgold) acts on the pane as a tile: four placement pills — the new pane opens LEFT, RIGHT, ABOVE, or BELOW — plus ZOOM (offered only while unzoomed; Esc is the way back) and CLOSE. *Binding* (butter) is a radio, not a set of actions: UNLINKED (the unmarked default), LINKED FS (a browser following this pane's regard at the directory level — an indicated file shows its directory, an indicated node its data space), LINKED VIEWER (a renderer for whatever this pane indicates, by content type). SPLIT is the one verb that creates a pane; the selected binding says what the created pane IS. *Context verbs* (orange; red for destructive) act on the content, per pane kind, starting lean: files carries HOME, BACK, DOWNLOAD, DELETE; the DAG carries ENTER NODE and BACK. Destructive verbs run as visible console commands — a DELETE is an `rm` in the transcript, auditable, never a silent mutation behind a pill.

This completes the control taxonomy: the mode frame holds display-content modes, the drawer's structure and binding groups hold composition, and its verb group holds content actions. The old VIEWER and BROWSE NODE verbs retired into the binding radio.

=== The mode frame (locked 2026-09-03)

Display-content modes — which layout, which projection, shape or census, rows or cards or previews — are meanings the operator flips while looking, and their capsules belong on the frame, not over the field: in Okuda's grammar nothing floats over the black field, and a pill riding on the graph is a web-map control in orange. But a standing frame column costs a capsule's width from every pane, one pill or six, and the drawer alone hides state and charges a gesture per flip. So the mode frame borrows the header's own law: *a frame that gets out of the way leaves a strip.* The field is framed at rest the LCARS way — a rule along its top turning through an elbow into a spine down its right edge, solid, in the frame's color (the files pane's rule is its path header's, made sticky so it never scrolls away). Touch the spine and it widens into the elbow's vertical bar, whose segments are the mode blocks — labels on the bar, black gaps between, a plain fill to the foot: the bar is the frame, there is no panel, and it costs field width, which is the incentive to put it away. Touch the field, or press Esc, and they retract. FILTER is one of the blocks on a roster's frame — filtering is a mode — and the strip it unfolds lives under the rule.

Four rules keep it honest. Open on *touch*, never on hover: a hover only lights the strip (a beckon), because every affordance is by touch and a hover-open would fire under every orbit and drag on the DAG. The open frame *overlays* the field as a transient like a drawer — it never reflows the render buffer — and it stands in the Esc chain beside the drawers. It rides the *same edge* on every pane that has modes, and a pane with no modes wears no strip (an empty frame behind a strip would lie). And the *bar annunciates* every non-default mode while the frame is away (`MOLECULE · 2D · CENSUS`, `CARDS`), nothing at rest — dark cockpit — so state never hides behind retracted chrome. The language is untouched: a verb lowers to the same capsule whether the frame is in or out.

=== The argus language (grilled, locked 2026-08-31)

Every gesture is a sentence. Subject-first verbs — `pane split right`, `pane bind viewer`, `view runs`, `dag projection 2d`, `file delete`, bare `back` — run client-side off a reserved-subject set and never touch the wire; any other line belongs to the session. Verbs act on the focused pane by default, `@id` names a pane explicitly, and `%n` names the n-th pane created during a replay. Fidelity is by construction, not convention: a verb clicks the same DOM control the mouse would — the drawer's pills, the rail's capsules, the gutter's givens — so sentence and gesture cannot drift apart.

The command line is prefix-`:` in the tmux tradition (double Ctrl-B works too): a floating single line with Tab completion over the verb table, Esc atop the contextual-back stack, results echoed into the console transcript. A non-argus line typed there falls through to the session, so one command line serves both registers. The console itself also speaks the language — scripts, transcripts, and executable help all live in text. The LANG capsule on the console bar is the mouse handle; the reserved gutter domains stay unburned.

A DESKTOP is a saved layout described in this language: a verb script, serialized from the live tree — shape, kinds, bindings, display modes — with `%n` ordinals and no concrete addresses, per the schema law; the regard supplies addresses at apply time. `desktop save|load|show|list|delete`. Storage is dual and honest: localStorage always, written through to `~/.config/argus/desktops/` on CFS via `config write` (the durable layer's write path), and a save against a daemon that predates the builtin says so instead of pretending.

`argus verbs` prints the living verb table.

== The durable layer: config and named schemas in CFS

Per-user preference and captured layouts live as files in the operator's CUBE home — the Linux dotfile tradition, on CFS (durable CUBE file storage, not the synthesized VFS overlays). `~/.config/chell/` holds prompt choice; `~/.config/argus/` holds color scheme and layout schemas. The kernel stores bytes and never interprets them; each surface parses only its own directory. This concretizes the workspace-model dumb-store principle with zero new kernel machinery.

A *named layout schema* is a captured pane arrangement — but the capture is a template parameterized by regard, never a materialized layout: tree shape, pane kinds, and binding modes (slaved/rooted), with no concrete addresses. Addresses flow in at apply time from the regard that triggers it. The old ChRIS UI's fixed four-pane feed view (DAG, runtime, browser, context — all slaved to the current node) becomes one user-editable schema file, offered in the drawer alongside semantic children when a node is clicked. Schema files are written in the LayoutNode vocabulary already shelved in `menu`.

Terminology guard: gutter presets remain *givens* (system-owned, reset semantics); named captures are *user schemas* (operator-owned, durable). The strata, bottom to top: retained cell (live) → session regard (session) → named schema (durable).

`~/gathers` is flagged as a further durable-CFS concept central to future operation; its definition is owed and deliberately not invented here.

== Longevity: the clean-room strategy, and PHAROS

Building a serious instrument on another studio's trade dress has a shelf life, and the risk is worth naming precisely. (What follows is an engineering posture, not legal advice; institutional counsel reviews before any of it is relied on.)

What CBS/Paramount can plausibly reach: the word "LCARS" itself (an asserted trademark — historical reference in documentation is commentary; product naming is exposure); and the signature ornament — the swept elbow silhouette, the salmon/orange/lavender palette families, the Okudagram numeric-caption idiom, reproductions of actual screens. What they cannot reach is the discipline: _Lotus v. Borland_ held an interface's method of operation uncopyrightable, and _Apple v. Microsoft_ left GUI elements largely unprotectable under the merger and scènes à faire doctrines. Rectilinear tiling, no-overlap, frame-as-control, color-as-role, panels-as-verbs — functional paradigm, free to all, and independently practiced by avionics long before the fiction. The sharper near-term risk is not litigation at all but adoption: an institutional deployment wearing recognizable franchise trade dress invites a compliance veto long before a studio notices.

The architecture already contains the answer: AEGIS-the-standard — the drawer, subjects, regard, the claim rule, the tiling algebra — contains zero LCARS. The costume is one theme over tokenized CSS. The strategy is therefore dual-track:

* *PHAROS, the house visual language, becomes the default.* It keeps the entire discipline and replaces the signature ornament: the chamfer (cut stone) where the elbow was, the beacon where color-wash attention was, a role-derived harbor palette (night harbor, limestone, beacon amber, sweep white, seaglass, port/starboard for direction), its own type voice. First light — a side-by-side design study against the current look, with the keep/replace verdicts — exists as a published study; the language is defined in `pharos.adoc`, and its first palette in the stylesheet is the clinical one.
* *LCARS demotes to an operator-supplied skin.* The existing theme-import mechanism (fetched to the operator's machine at their choice) is exactly the right legal posture: the operator themes their own console, as with any desktop; the theme never ships in the repo. This also resolves the vendoring question around TheLCARS.com assets — they never enter the tree either way.

=== PHAROS, expanded

PHAROS carries two readings, both true. The shallow read is the recursive phrase: *Presentation House for AEGIS — Roles, Ornament, Shapes*. The deep read is an inventory — six letters, the six dimensions a visual language must define, and the chapters of the future style guide:

* *P*alette — color as role, never decoration.
* *H*ierarchy — type scale, weight, and the case law (uppercase for labels only; content keeps its case).
* *A*ffordance — what reads as operable.
* *R*hythm — spacing, cadence, motion (the beacon's sweep period, the drawer glide).
* *O*rnament — the motifs: beacon, light line, tier.
* *S*hape — the grammar: chamfer, capsule, divider.

=== The naming law

The fleet has two naming registers, and each has its own recursion style. *Kitchen names* (cumin, salsa, chili, brasa, chell — the mise en place) are ingredients, and each is a *self-recursive* backronym, GNU-style, per the table in the root README (*Cumin Underpins Management Infrastructure Needs*, *BRASA Runs Abstracted Shell Actions*, ...): a closed definition of its own function, with the layering left to the architecture docs. *Harbor names* (ARGUS, AEGIS, PHAROS, CALYPSO, HARBOR) are charters, and each expands *chain-recursively* — through the layer beneath it — so the presentation stack's dependency reads straight out of the expansions:

* *PHAROS* — Presentation House for *AEGIS*: Roles, Ornament, Shapes.
* *AEGIS* — *ARGUS* Ergonomics & Graphical Interface Standard.
* *ARGUS* — the All-*Regarding* Graphical User Surface: the hundred-eyed watchman named for what it does, and "regarding" is meant literally — *regard*, the session subject, is what the surface projects. The skin names the standard, the standard names the surface, the surface names the session truth it renders.
* *CALYPSO* — *CALYPSO* Accepts Language, Yielding Permitted Shell Operations: the junction node. CALYPSO sits in both registers — kitchen package and harbor keeper — so it is self-recursive by kitchen convention, and precisely thereby the fixed point where the harbor chain grounds. The daemon is where the kitchen serves the harbor; the names encode it.

Expansion dependency mirrors actual dependency; a new harbor name that cannot say which layer it expands through is a sign the layer itself is unclear. And exactly one fixed point: a second self-recursive harbor name would mean a layer has lost its footing.

== Precedents and references

Every load-bearing claim in this document leans on prior art. The precedents, what each grounds, and where to read them:

* *X11 primary selection — selection is server truth.* The selection mechanism lives in the X server precisely so any client can consume an indication without the pointing client knowing who listens; grounds the promotion of *regard* to brasa. David S. H. Rosenthal, _Inter-Client Communication Conventions Manual_ (ICCCM), MIT X Consortium, drafted 1988 (X11R2); v2.0, X Consortium Standard, 1994, ch. 2 "Peer-to-Peer Communication by Means of Selections". https://www.x.org/releases/current/doc/xorg-docs/icccm/icccm.pdf
* *Plan 9 plumber — the indicated thing is system truth.* "The thing the user pointed at" is a first-class, system-wide message; ecosystems (plumb anywhere) exist only because it was promoted from day one. Rob Pike, "Plumbing and Other Utilities", _Proc. USENIX Annual Technical Conference_, San Diego, 2000. https://9p.io/sys/doc/plumb.html
* *acme — the single instrument with subdivisions.* A tiled, mouse-verb interface with total jurisdiction over its panes, not a manager of foreign windows; grounds ARGUS's desktop framing. Rob Pike, "Acme: A User Interface for Programmers", _Proc. USENIX Winter 1994 Technical Conference_, San Francisco, January 1994. https://www.usenix.org/legacy/publications/library/proceedings/sf94/full_papers/pike.pdf
* *tmux — verbs behind a modal gesture.* Zero visible chrome; every layout verb lives behind the prefix key; grounds the pane drawer as the mouse-native prefix. Nicholas Marriott et al., `tmux(1)` manual, OpenBSD.
* *Tiling window-manager culture — the exiled frame.* dwm and i3 shrink decoration to a one-pixel border; the contrast grounds "the titlebar never died here, it got promoted". suckless.org dwm documentation; i3wm.org user's guide.
* *Airbus ECAM and flight-deck color law — color as role, not decoration.* Certified cockpits assign colors by semantic role (red = warning, amber = caution) as regulation, not style; grounds the palette-roles thread and the "discipline proven under other names" claim. FAA 14 CFR §25.1322 "Flightcrew Alerting" (Amdt. 25-131, 2010) with Advisory Circular AC 25.1322-1 (December 13, 2010: red = warning, amber/yellow = caution, colors restricted to those roles) and the harmonized EASA CS-25.1322; Airbus A320 FCOM display-system design philosophy (the "dark cockpit" doctrine: at rest, annunciate nothing).
* *Radiology MPR and sync groups — subjects, not edges.* Orthogonal slice viewers synchronize through a shared crosshair position in volume space, and linked scrolling is explicit group membership, not viewer-to-viewer wiring; grounds hub-and-spoke linkage. Rosset A., Spadola L., Ratib O., "OsiriX: An Open-Source Software for Navigating in Multidimensional DICOM Images", _J. Digital Imaging_ 17(3):205–216, 2004. doi:10.1007/s10278-004-1014-6
* *Fitts's law — the border as target.* Target acquisition time falls with target width; a full-width top border is the cheapest large target a pane owns. Paul M. Fitts, "The Information Capacity of the Human Motor System in Controlling the Amplitude of Movement", _J. Experimental Psychology_ 47(6):381–391, 1954.
* *Interface law — the functional carve-out.* _Lotus Development Corp. v. Borland International, Inc._, 49 F.3d 807 (1st Cir. 1995), aff'd by an equally divided Court, 516 U.S. 233 (1996): a menu command hierarchy is an uncopyrightable "method of operation". _Apple Computer, Inc. v. Microsoft Corp._, 35 F.3d 1435 (9th Cir. 1994): GUI elements largely unprotectable under merger and scènes à faire. Together they ground the keep/replace line the PHAROS strategy draws: discipline free, signature ornament not.
* *LCARS itself.* Michael Okuda's display language for _Star Trek: The Next Generation_ (1987–); "LCARS" and the screen designs are the property of CBS Studios / Paramount, referenced here as prior art. Rick Sternbach & Michael Okuda, _Star Trek: The Next Generation Technical Manual_, Pocket Books, 1991. Okuda's later real-world design work for NASA — mission patches and insignia, honored with the Exceptional Public Service Medal (Johnson Space Center, July 9, 2009) — is the closest the language's author came to operational practice. The ARGUS visual tokens derive from Jim Robertus's TheLCARS.com theme work.
* *The cosmetic graveyard.* LCARS skins, launchers, and dashboards (Rainmeter suites, Android LCARS launchers, Home Assistant themes, TheLCARS.com templates) — cited as the pattern AEGIS breaks: appearance cloned, mechanics absent.

== Open threads

* *Palette roles.* The LCARS tokens exist (`--pumpkin-pie`, `--harvestgold`, `--butter`, `--orange`, `--honey`, `--october-sunset`, `--daybreak`, `--sky`) but are used by feel, not by rule. AEGIS should assign semantic roles — structure, actionable, state/attention, data — and then audit the CSS against its own canon.
* *Shape grammar.* Capsule, bar, elbow, pill-toggle, divider: a small closed vocabulary; no new shapes without a new category of meaning.
* *Motion canon.* Motion only for continuity of place, never ornament; duration tokens (the 600ms zoom settle is currently hard-coded).
* *Uppercase: law or habit.* Inherited from the prop; unexamined.
* *Sound.* In-fiction LCARS chirps. Reserved and off by default, or banished.
* *Drawer mechanics.* The border affordance, the open/close glide, keyboard path, first-run teaching.
* *Slaved-viewer lifecycle details.* What a viewer shows before its first regard value; whether it can re-root to a different subject.
* *Violations audit.* An honest standing list of where the shipped surface breaks its own canon (the header capsules are the first entry).
