Repository navigation
levain 0.9.0
Two things ship together: the signed team tier (team ledgers signed by SSH keys, the team replayed from signed history) and the live cockpit (eleven panel kinds, a page that redraws only what changed, an ambient sky).
Team ledgers are signed, and the team is derived from the ledger's signed history. Breaking: 0.6.x team ledgers are not read.
Changed
- A member is an SSH key, not an email. Every commit on the team ledger is signed with the writer's SSH key (ed25519 or ecdsa), and levain verifies each signature by running
ssh-keygen -Y check-novalidateitself, sogpg.ssh.allowedSignersFile,gpg.ssh.programand similar repository settings cannot change a verdict. A signature that fails to verify reads as unsigned, provided a known-good signature still verifies on the same machine at that moment; a check that cannot run (nossh-keygen, a timeout, anssh-keygenthat fails on the known-good signature too) is never cached and never read as signed or unsigned, and the hook then denies edits it cannot judge. - New branch,
levain-team-ledger, withtenure.tomlbesideteam.toml. The genesis commit carries both files and is signed by the owner's key thattenure.tomllists.levain team initandlevain team jointake--signing-key.joinpins the genesis and prints the owner and key fingerprints in force, to check with the owner out of band. Runningjoinagain keeps the clone on the genesis it pinned, with its distrust list; it moves to another genesis only when--rootnames it. - The team is replayed, not read off the tip. A change to
team.tomlortenure.tomlcounts only if the key that signed it may make that change at that point in history, and it must name the last counted change (Levain-Basetrailer). A ledger line is enforced only if it was signed by a key in force, at that point, for the member it is filed under (the owner, for pack lines).levain team statusshows recent role changes, a pending hand-off, keys waiting for confirmation, and the reason when verdicts are frozen. - Keys:
levain team key add | confirm | remove. A proposed key comes into force only when a commit signed by that key confirms it (join, orlevain team key confirm, on the machine holding it). A member proposes their own keys from a machine already in force. The owner proposes only a member's first key, and a removed member's handle is never added again, so no key the owner holds can come to sign as a handle that has signed before. A member who lost every key is invited again under a new handle. - Ownership:
levain team owner <handle>offers ownership to a member andlevain team accepttakes it, signed by a key already in force for that member. An offer carries no keys, and it lapses if its member is removed. - Recovery:
levain team revoke <fingerprint> --after <commit>(the owner) voids every line a stolen member key signed after that commit (the honest holder's included), which must be in the current owner's spell (an earlier one is refused, with the reason). An owner key is never revoked in the ledger: whoever holds a stolen owner key controls the ledger until a re-genesis.levain team distrust <commit>stops this clone counting one commit at once.levain team veto <handle> --role member|owner --since <commit that began the spell>(the owner) withdraws a past spell's authority: for a member's spell, every line that member's folder gained during it stops being enforced, which removing the member or revoking a key does not do for lines already signed; for an owner's spell, only its authority to supersede or retire others' entries (its pack lines andPROJECT.mdstill count).--sincemust be the commit that began the spell, or the veto is refused. A merge on the ledger branch freezes verdicts (from the last commit this clone judged before it) untillevain team accept-merge <merge> --parent N, run on the owner's word (--parentis required, andjoin --accept-mergetakesSHA:N: which parent counts is never a default). A sync re-publishes only commits this clone wrote (each carries aLevain-Devicetrailer), never one another clone of the same key published, and it never re-signs entries with a key that is not in force: those wait underrefs/levain/held/untillevain team key confirm; while the ledger is frozen, a held entry is re-signed only if the key counts both in the frozen view (the last commit judged) and in the published history judged with no anchor, as a clone joining now would judge it. Held entries and offline team changes are bound to the genesis they were made on: afterjoin --rootto another ledger they are kept and named, never replayed or re-applied there. A history rewrite is detected and never re-published; verdicts stay frozen at the last commit judged on the old history untillevain team repin.levain team regenesisstarts a fresh signed genesis on a new branch when the owner is permanently gone or the owner's key is stolen. The old ledger files at the old tip are carried as a read-only record that is not enforced (rulings are recorded again), and whoever runs it is the new owner, signing it with their own key (it runs only from a joined clone, from a commit of the ledger that clone is pinned to and has judged (an ancestor of its anchor or of its last counted team change), and an--ownernaming a handle the old ledger knows is refused unless the runner's key is in force for it), so members check the fingerprintsjoin --rootprints.levain team statusshows a re-genesis branch with its claimed owner and keys, as unverified. - 0.6.x ledgers: levain v2 refuses to read the unsigned
levain-ledgerbranch. The owner starts again withlevain team init --replace-legacy, re-records the entries to keep, and deletes the old remote branch withlevain team retire-legacy. levain team exportwrites anneal's team stream contract v3 (theprev_rootheader and the strict profile). Its import into anneal-memory waits on the anneal release that reads v3; until then the session-start import uses the earlier stream and says that an anneal-memory release reading stream v3 is needed (the version is named once it ships).
Known limits
- A git host can withhold newer ledger commits from a clone, and the clone cannot tell. Signatures prove who wrote what the clone sees, not that it sees everything. The planned fix is the team server's transparency log.
- The owner can invite a new handle under any key the owner chooses, as with any admin-run identity system: the fingerprint check at
joinis a person's job. - Role-change lines in
levain team statusare filtered by the commit's author date, which the writer sets. - While verdicts are frozen (a merge, a rewrite), nothing recorded after the freeze counts, team changes included (a revoke made then is not in force), and the edit hook does not stop the edits those waiting rulings govern;
statuscounts the waiting lines but does not list them. The hook denies only when it cannot judge at all, unlessLEVAIN_TEAM_UNJUDGED=allow. - A clone holding only a 0.6.x
levain-ledgerenforces nothing until the owner migrates (edits go through with a one-line notice).
Cockpit: eleven panel kinds, a live page, an ambient sky
The cockpit's panel vocabulary grows from four kinds to eleven, the page stays current without a reload, and the board gets a new look. The schema string is now levain.cockpit/2; a server written against /1 keeps working (see "Changed").
Added
- Eleven panel kinds.
reading,line,brief,count-queue,ambient,status-dots,metric-grid,roster,list,formandlog. Each has a fixed shape (written up indocs/cockpit-contract.md); data that does not fit makes that one panel show an error naming the panel and the field, never a half-drawn card. The terminal view and any renderer without a component for a kind draw it as text. - A filter box on long panels. Any panel that is a list, a queue, a roster or a log gets a filter box once it holds more than 8 rows or declares a window. Type to narrow; terms are matched ignoring case and accents, all terms must match, and the text is plain (
.*means those three characters). A panel that shows only the newest N says so ("searches the newest N only") unless its provider can search the whole store, in which case the box asks the store and labels the result ("41 matches in the store"). A search that fails or times out says so; it never reads as "no matches". Search text is kept out of the server's journal and its access log. - A live page. The page opens one change stream (
GET /cockpit/events) and redraws only the panel that moved, in place, without a reload: your scroll position, an open filter and a "show all" stay where they were. The stream carries no panel data, only which panel changed. A small chip in the header says where it stands:connecting,live,paused(the tab is hidden; a hidden tab closes its stream and catches up when you come back),stale since HH:MM(the server went away; the page retries with a growing delay and polls every 30 seconds meanwhile), ornot live(the page never got a first draw). A server holds at most 8 streams at once; a ninth is told it is busy. - The ambient sky. A panel of kind
ambientdraws the last 14 hours of events (commits, reviews, memory writes) as a timeline: a row per repository with the busiest rows named and the rest folded into "other", failures in orange, and a short meteor when a new event lands. An "Ambient: on / paused" button stops the motion, and the page honours the operating system's reduced-motion setting. The animation stops entirely when nothing is moving. - A mode bar and a jump box. Morning, Board and Build modes in the header, and a jump box (Cmd-K) that goes to any panel by name.
- Read-age chips. A panel shows how long ago its data was read, and the chip updates in place when the panel does.
Changed
levain.cockpit/2renames the four/1kinds.triage-listis nowlist,metricisstatus-dots,visualisambient,proseisbrief. A provider that still declares the old name is accepted and its panel carries the new one; the data shapes of the four are unchanged. The schema string is part of every ETag, so a browser holding an older cockpit's copy fetches a fresh one rather than reusing it.- The board is redrawn panel by panel. A panel that changes shape (a list that becomes a card, say) is rebuilt; one that keeps its shape is updated where it stands.
Known edges
Each of these was found in review, judged not reachable in normal use, and left alone. None loses data.
- Searching and the filter box
- A search with a pasted trailing newline is accepted as its trimmed text.
- Searches are kept out of the access log by the name of their parameter (
q=). A query sent under an escaped spelling of that name (%71=) is logged. Only someone typing such URLs by hand in a verbose local log reaches it. - A query made only of accent marks matches every row.
- A search URL with an unencoded space in it can give a less helpful error message.
- In the filter box the text survives a panel update only through the browser's session storage; with storage blocked, the filter clears when the panel redraws.
- After a panel updates, old store-search results stay on screen until the new search returns; there is no "searching" flash.
- The jump box shows a limited number of entries, which can push a "Search ..." entry out of view.
- Panel data a provider could send
- A panel value that sends an explicit
nullfor an optional field (value,unit,read,silence_is_signal) is read as if the field were absent. - A
briefthat carries markdown still has itslineschecked. - A panel declared with
legacy_kindis trusted: that argument is for our own code. - A provider's
as_ofthat is not a valid timestamp makes/cockpit/freshness.jsonanswer 500. This is how that route behaved before this release and the live page no longer reads it.
- A panel value that sends an explicit
- The page
- A panel read for the first time with no read time has no read-age chip until the next full draw. A panel opened in the focus window shows no chip at all.
- An expanded long row folds back when its panel redraws.
- A stale panel's banner age is part of its shape, so a stale panel that changes redraws as a whole.
- A panel whose update loses its typed form leaves the old body in an open focus window until you close it, and the window's title is not refreshed.
- Reached from another machine, after the write token has been rotated, two requests refused at the same moment can each raise the token prompt.
- A panel that is not given a mode forces the Board view when you jump to it.
- Update events arriving faster than the page can fetch can delay the repaint; the server folds them per panel, which keeps this rare.
- The sky
- Event times must be extended ISO-8601 (
2026-10-12T01:00:00Z) or carry no zone, in which case they are read as UTC. The compact form (20261012T010000Z) passes the contract but is not drawn correctly. - A feed that sends a
nullevent, a repository named__proto__orconstructor, or a non-numeric window length breaks the sky; our own feeds never do. - On time zones that are not on the hour (half-hour offsets), the hour labels sit slightly off.
- An event dated in the future never gets a meteor.
- Opening one repository's view can still draw meteors for other repositories.
- A browser without
ResizeObserverdoes not remeasure the label gutter when the window resizes.
- Event times must be extended ISO-8601 (
- The live stream
- The watcher reads panels under one access class. A change visible only to another class (a change in what a signed-in action may do) is not announced by the stream; the page picks it up on its next reload or reconnect.
- A server whose connection is blocked mid-write notices a gone browser only after the socket timeout.
- The first read when a stream opens on an idle server holds the watcher's lock while it runs, which can delay the first announcement.
- After a stream comes back, the chip says
not liveuntil the first message arrives, a matter of seconds. - Two refetches of one panel that overlap, with the older answer arriving last, followed by a disconnect and an exact revert of the panel, can leave that panel showing the older content until its next change.
- A refetch in flight when the tab is hidden that then fails keeps retrying on the hidden tab, on the normal retry schedule, and is not carried over to when you return.