Releases: Acquiredl/loxodonta
Release list
v0.7.0
The release that came out of three outside reviews, all machine-written, of the public repo at v0.6.0. Most of what they observed was already true and stayed so, what they got wrong went nowhere, and three things they noticed in passing were real: a flagless install said nothing about the tier it left you on, every append re-read the whole chain for a check most receipts did not need, and the dashboard sent two headers fewer than a browser deserves. Two more came from how the reviews explained the tool back. They named the harnesses and drew the parts in their first lines, and the README did neither above the fold.
Added
install-hooksays which tier a flagless install is on (#221). A plain install wires local-only recording: an edit, a deletion, or a reorder of a receipt is caught unconditionally, a chain regenerated from scratch only against a head kept off the machine. Until now that went unsaid unless a flag was present. One line now, on first install and on every re-run, for Claude Code and Codex alike; the Codex line names the one opt-in it has, since it has no session-end anchor (ADR-0024). The opt-in posture itself is unchanged, anddocs/START.mdstep 2 says the same in one paragraph.- The dashboard sends
X-Content-Type-Options: nosniffandContent-Security-Policy: frame-ancestors 'none'on every response, refusals included (#225). Loopback-only listening, the Host and Origin refusals, and a page that never touchesinnerHTMLwere the defenses that mattered; these two close the browser behaviors they did not cover. A full policy would need a nonce, since the page is all inline script, and is held out.
Changed
- The case-only respelling check runs only for receipts that carry files (#222).
append_lockedparsed every line of the chain on every append to build the set of paths ever referenced (SPEC §3), and most hook receipts carry no files and paid for it anyway. Measured through the CLI on a 3,878-entry chain, a file-less append went from 157 ms to 146 ms median, whole process, interpreter start included. The check stays at append time, on the machine whose filesystem knows the answer (ADR-0026), and a test guards the one way the guard could be wrong: file-less entries between two spellings must not lose the warning. - The README names its harnesses and its parts on the first screen: one line under the badges for Claude Code, Codex CLI, and the OpenAI Agents SDK, and a three-item map of recorder, reader, and translators between the demo and Install. The one-file sentence in How it works says which file it is true of, the recorder, and no longer counts its commands. Structure borrowed from the reviews; their phrasing was not, since it leans on the words this repo refuses.
v0.6.0
The release that came out of walking the front door as a stranger. Following the five steps in order left a new installer with nothing judged at all, because the supervisor could date the beginning of coverage no earlier than its own first look and the page puts a week of work before that. The recorder knows when coverage begins, since it is the thing that wires it, so now it writes that down. Three smaller faults on the same path went with it, each of them something a reader met in their first two commands.
Added
- The coverage marker (ADR-0030):
install-hookappends what it wired to~/.loxodonta/coverage.jsonassince,matchersandharness, one entry per change and nothing on an unchanged wiring. Every scan reads it fresh, merges it in memory, and never copies it into the baseline, which goes on holding only what the supervisor observed. A marker may fill time before the supervisor's first observation and never reach past it, the rule ADR-0029 already applies to an operator's seed; on the same instant the operator's word wins. Epochs now name their source, and any surface that judged a session by a non-observed one says so.uninstall-hookwrites nothing, because a start claim says more calls owe receipts while an end claim says fewer, and "nothing was owed from here" is the silence the completeness alarm exists to catch. A store with no marker behaves exactly as before.
Fixed
- Following
docs/START.mdin order left a new installer with zero judged sessions. The page asks for five steps: wire the hook, work for a week, then scan. ADR-0029 stamps the calibration inception at the supervisor's first look, which is that scan, so every session recorded during the week fell before the memory watching it. Walked against the published files in a sandboxed home, install-work-scan gave 0 of 3 sessions judged; install-scan-work-scan gave 2 of 2. Closed by the coverage marker above, which dates coverage from when it was wired instead. - The empty-store scan note told a reader to run
loxodonta install-hookeven when the hook was already wired, so anyone who finished step 2 ofdocs/START.mdand scanned out of curiosity was told to redo the step they had just done. An empty store means "nothing has run yet" when the hook is wired and "nothing is recording" when it is not, and those ask opposite things of the reader; the wired matchers tell them apart. - The install step in the README and
docs/START.mdshowed the version line ascommit ..., as though a hash would appear there. A file downloaded from the releases page printscommit unknown, because the version line reads the commit from the git checkout the file sits in and a download sits in none. Every reader following step 1 correctly met that word on their first command with nothing saying it was expected. Both pages now show the real output and say why.
v0.5.0
The release where the witness stops claiming more than it saw, and starts seeing all of what it should. It judged sessions older than its own memory by today's coverage, scarring history that was recorded honestly under older rules, and it read only a session's parent transcript, so every tool call a subagent made came back as a receipt nobody owed. Both were loudest on the surface a stranger meets first: a fresh install reported the whole pre-install history as loss.
Added
BEFORE-MEMORY, a completeness state for a session whose first witnessed tool event predates the supervisor's first calibration observation (ADR-0029, issue #114). What such a session owed is unknown, so it is judged not at all: evidence, not deficit, and no receipt of its is called missing. It never takes a row. One counted block reports it on the scan, on the dashboard beside the calibration note, in the field-data export and in a package'switness.json, because a store older than its supervisor holds scores of these and a listing they fill is one where the sessions that mean something cannot be found.supervisor scan --before-memorylists them for anyone who wants to look.supervisor calibrate --since <ts> --matchers <m>, for stating what coverage was wired before the supervisor started watching. It inserts an epoch only ahead of the first observed one and refuses anything later with no--forcebehind it, because observed time is the one part of the calibration memory that is not testimony. A seeded epoch is marked as the operator's word permanently, every surface that judged by one says so, and--forget <ts>withdraws a statement without hand-editing a writer-reachable file.--rootmirrorsscan's.docs/START.md, the page for someone who has already decided to run the tool: five steps from the releases page tosupervisor export, with the agent install offered above them as a copy-pasteable prompt for the Claude Code or Codex the reader most likely already has in their terminal, and the plain statement of what it means to ask an agent to wire up its own recorder. The README keeps the front door for the undecided and carries one pointer to it under Install.tools/house_check.pyjudgesSTART.mdas a front-door file although it lives indocs/, where em dashes and hedged overclaims are otherwise allowed, because it is read before any trust has been extended, which is why that rule exists; one test pins it.
Fixed
- The completeness witness read only a session's parent transcript, so every tool call a subagent made counted as an unowed receipt (#211). The harness fires
PostToolUsefor a subagent's calls under the parent session id, so their receipts land in the parent's chain while the record of them sits in<session>/subagents/agent-*.jsonl. A session that delegated therefore readENDED-SURPLUSfor work it did honestly, and the ingest leg ADR-0016 widened coverage to capture went missing from the completeness picture altogether, because a delegating parent spawns and writes while its subagents read and search.witness_filesnow reads the sidechain beside the parent. The pairing rule is the one already there: a sidechain result carries thetool_resultblock and not thetoolUseResultfield, so keying on the field alone found 57 of one session's 3244 sidechain calls, and the two shapes agree wherever both appear across every transcript in the author's store. Every surplus in that store closed; four of the seven land exactly, the largest at 3721 against 3721, and the three that do not are all from before the cross-drive hook bug was fixed on 2026-08-30. - The witness judged sessions from before its own memory by today's matcher (issue #114).
calibrate()wrote its first observation as{"since": null}andmatchers_at()gave that epoch to every earlier timestamp, so on a machine whose calibration memory began after coverage went wide every session recorded under the narrow matcher was re-judged by*. ADR-0016 ruling 2 promised a matcher change would manufacture no scars; this was the one case its mechanism could not see, because the change predated the memory. The first observation now stamps the moment it is made, and a store carrying the oldnullhas it stamped on the next scan. On the author's machine that turned 133 witnessed sessions with 116 deficits into six judged rows and one counted line, and it is what a stranger's firstsupervisor exportstops saying about their pre-install history.
Changed
- ADR-0022 ruling 4 is amended by ADR-0028: a promotion to
mainthat changes neitherloxodonta.pynorsupervisor.pycarries no tag and no release, because the tool version says which recorder and supervisor a person is running (ruling 3) and a documentation page moves neither. The test isgit diffover the two files. Promotions that move either tool are unchanged, as is the patch path for a hotfix cherry-picked tomain, and the release workflow's tag-matches-TOOL_VERSIONgate is untouched since it runs on a tag and such a promotion pushes none.
v0.4.0
The release where the reader starts counting. The dashboard gains an activity tab over what the chains already hold, and the hook is asked for nothing new to draw it. Three lines that were teaching the operator to ignore the tool are fixed: an anchor that stayed pending after it had settled, a scan that raised the flagship alarm out of a wrong invocation, and a suite that ran only one way.
Added
- The dashboard's activity tab (ADR-0027, #200 and #201): the reader counts what the chains already hold, and the hook is asked for nothing new to draw it. Activity moves out of the second pane's inspect toggle to a fifth tab beside sessions, projects, search and evidence, carrying seven panels under a cap of two screens at 1440x900, measured as document height rather than set as a number. The panels: the tally (drawers, sessions, chains, receipts, and the date recording began), tempo against the store's own norm with the distance past the bar shown rather than one word for every hot session, looks per day, a tool histogram, files touched with the worktree prefix folded so one file is one row, working hours, and sessions on one time axis in a bounded box that scrolls inside itself with owed tails sorted to the top. The inspect pane gains a density strip: receipts bucketed across a session's span, the peak marked, the owed tail hatched, because a session of 3,721 receipts has a shape no waterfall can draw. Every panel names its own window and there is no global range picker, since a picker some panels quietly ignored would be the page lying about what it shows. Named views are saved in a sibling dotfile beside the day book and share its posture: writer-reachable, trusted for nothing, owning no verdicts, never raising an exit.
SUPERVISOR_HOT_TIMESandSUPERVISOR_HOT_FLOORstay the only place the hot threshold is set, and are surfaced on the panel. No count raises the scan exit, and.out-of-scope/001stands unamended. GLOSSARY gained Tally. tools/publish_echo.py: a loopback echo that prints what--publish-headposts, with the field names beside each body, so an operator reads the claim in ADR-0025 for themselves before wiring a real remote.--port 0binds a free port and prints it,--oncestops after one body. It says on startup, anddocs/HOOK.mdsays with it, that an address on this machine is not a head record and never a place to publish to: the head has not left, and nothing there is out of the writer's reach.
Fixed
verify --anchorsno longer advisesanchor --upgradeon a head another calendar has already settled (#199). The suppression was keyed by head and calendar while its comment said head, so on a store anchored to the four default calendars a settled head printed one pending line per calendar that lagged, each advising a command that changed nothing, above the lines settling that same head. An anchor's claim is about the head, so a straggler's record is now reported asANCHOR-UNANSWERED, naming the calendar that never came back and saying no upgrade is owed; the record stays in the sidecar as evidence of where the submission went.ANCHOR-PENDINGand its advice are unchanged for a head nothing has settled.anchor --upgradeskips a settled head instead of re-asking a calendar that has dropped its commitment, and says why. Through the verdict-line seam this empties the scan's pending list for such a chain, so the dashboard stops painting anchor staleness on an anchored head.verify-packagecarried the same shape over the manifest digest and now saysANCHOR-UNANSWEREDthere too.supervisor scan --root DIRno longer raises the completeness alarm over sessions whose receipts live in the store (#117).--rootwalks a legacy folder of repos forreceipts/folders (ADR-0011), and a machine that has adopted its chains into the store has none left there, so legacy pairing charged every transcript under the root its whole witnessed count: on the author's machine, 111 sessions readENDED-DEFICITwith zero receipts and the live one readALARM-SILENT, exit 6, while a plainscanof the same machine in the same minute exited 0 with the session OK. A wrong invocation should not fake the flagship claim. A witnessed session with no chain under the root is now checked against the store before it is judged: a chain there means the receipts exist and the wrong universe is being scanned, reported as the newELSEWHEREstate, named and not judged, with a note saying to runscanwith no--root. A session with no chain in either place is still the disabled hook and still alarms, which is what the watch is for. A--rootholding no chains at all also says so above the report.
v0.3.0
The release that makes the evidence portable: a session or a drawer packs into one file, and one command on a machine that has never seen yours judges it layer by layer and says what each layer does not settle. Chain heads can leave the machine as a session ends, to a remote the credentials here cannot delete from.
Added
supervisor package <session-id | entry-address>(ADR-0026, #176): one session as a package, siblings included: every chain and its anchor sidecar, the drawer's project record,witness.json(the completeness row and the scan's verdicts, labelled testimony in the file), a plain-words README, andmanifest.jsonwritten last, formatloxodonta-package/1, chains listed by head and entry count, every other file by sha256 and byte count,"seals": []declared. A zip by default,--folderfor an unpacked folder.--anchorand--signare later slices.supervisor package --repo PATH, and the current repository's drawer when no selector is given (ADR-0026 ruling 1, #178): the second unit, a whole drawer, every session and every sibling, resolved the waydigest --reporesolves a repository (CLAUDE_PROJECT_DIR, else the current directory) and including the drawers of its harness worktrees (ADR-0023). The manifest's unit is{"kind": "drawer", "project": ..., "sessions": N};witness.jsonnames its unit and carries one completeness row per session, a list for both units; the README says how many sessions and chains travel and lists chains by session. A session whose chains sit in more than one drawer refuses the whole drawer package, naming the session, so half a session never ships quietly. A selector and--repotogether exit 64. Store-wide packaging staysexport --raw's job. The drawer package is named for the drawer folder, the slug, which is safe on every filesystem and never collides; a repository of its own checked out under .claude/worktrees/ is not packaged as a worktree.loxodonta verify-package PATH(ADR-0026): judges a zip or a folder layer by layer, the recorder's ownverify --anchorsoutput verbatim per chain, each artifact against the manifest, then the package verdict with its stated limit and one line of residual trust. Exits map ontoverify's: 0SELF-CONSISTENT, 1CHAIN-BROKEN, 2ARTIFACT-DIVERGED, 4UNSUPPORTED-FORMAT(a refusal); gravest wins. Chains are judged by walking, never by file hash, so a Windows unzip that changes line endings still verifies.docs/PACKAGE.mdhas the format and the ladder. A package is refused, never judged, when it could not be read safely: every manifest path must be a bare file name, a zip declaring more than a gigabyte unpacked or damaged past its end record is refused unopened, and a manifest whose shape is off is refused unread. The verdict is the last line.supervisor package --transcript(ADR-0026 ruling 2, #179): the harness transcript ships on request only, because the chain holdsRead: .envwith a fingerprint and the transcript holds the contents of.env, so packaging it can hand the recipient the very secret a session exfiltrated. With the flag, each packaged session's transcript, the one the completeness watch paired with it, travels while it is still on disk astranscript-<session>.jsonl, listed by sha256 and byte count like any post-close artifact and named on the session's chain listings ("transcript", siblings sharing it); the README says, per session, whether it is present and if not why: not requested, or no longer on the packing machine.verify-packagejudges every transcript commitment in a chain against the packaged transcript the wayverify --transcriptdoes, the recorder's lines verbatim:TRANSCRIPT-DIVERGED, exit 5, when a committed prefix differs, outrankingARTIFACT-DIVERGED; when the chain holds commitments and no transcript travelled,TRANSCRIPT-UNRESOLVEDand no verdict. A chain listing naming a transcript that is not a bare file name, or that the artifacts do not list, is refused.verify --transcriptitself now states the tail after the last commitment in bytes, the window the chain never vouched for.supervisor package --anchor [--calendar URL]...(ADR-0026 rulings 4, 6 and 7, #180): the package's first seal. After the manifest is written, declaring"seals": ["anchor"], its sha256 goes to the OpenTimestamps calendars once through the recorder's newloxodonta anchor --manifest PATH, and the proof ships beside it asmanifest.json.anchors.jsonl, an ordinary anchor record whoseheadis the manifest's digest and which has non; in the zip it is the member after the manifest.loxodonta verify-packagejudges the manifest anchor offline the wayverify --anchorsjudges a chain's: a completed proof prints its block height and merkle root for the recipient to confirm and earnsSELF-CONSISTENT + ANCHOREDon the verdict line, with a residual-trust line that says the package existed by that block; a pending proof leaves the rung unearned with a note, andloxodonta anchor --upgrade --manifest PATHcompletes it later; a declared anchor with no sidecar isSEAL-MISSING, and a record for another digest, or one whose proof does not replay, isSEAL-INVALID, both exit 3. Chain anchors from session end stay detail under each chain and never earn the package rung. What leaves the machine is the manifest's 32-byte digest, to the public calendars, only with the flag; without it the manifest declares[]and nothing is posted; a calendar that accepts nothing leaves no package behind.supervisor package --sign KEYFILE(ADR-0026 rulings 4, 6 and 8, applying ADR-0008; #181): the package's second seal, the issuer signature, alone or with--anchor. After the manifest is written, and before the anchor when both are asked for, so a signing that fails costs no calendar submission, the supervisor hasssh-keygen -Y sign -n loxodonta-packagesign the manifest's shipped bytes with the SSH private key at KEYFILE and shipsmanifest.json.sigbeside it withmanifest.json.pub, the public key, which is testimony:KEYFILE.pubwhen it exists, else whatssh-keygen -yderives, its two tokens only, since the comment is a name. Neither tool reads the private key; only ssh-keygen is run with it, and its own prompts, a passphrase or a hardware touch, reach the terminal. The manifest declares"signature"inseals, after"anchor"when both; in the zip the signature, the key, and the sidecar follow the manifest in that order. Before anything is written the supervisor verifies the signature under the key that ships, so a staleKEYFILE.pubis refused at packaging instead of shipped; a~in KEYFILE means home on every shell. A signing that fails leaves no package behind, with ssh-keygen's own words.loxodonta verify-packagejudges the seal throughssh-keygen -Y verifyunder the shipped key and on success printsseal signature: SIGNED (key: SHA256:<fingerprint>)and adds+ SIGNED (key: ...)to the verdict line, after+ ANCHOREDwhen both hold: the fingerprint and never a name or the key's comment, for the recipient to compare against a channel the package cannot rewrite. A signature that does not hold, or a shipped key that is not the one that signed, isSEAL-INVALID; a declared signature with no.sigor no.pubisSEAL-MISSING; both exit 3. A recipient whose ssh-keygen is missing, cannot run, or predates-Y verifyis told the signature was not judged, and the ladder reports the rungs it could judge. The residual-trust line names what each seal gives, when and which key, and what still rests on the issuer's word.docs/PACKAGE.mdcarries the custody cage: a key any process of this user could use without the operator, unencrypted in~/.sshor loaded inssh-agent, is writer-reachable and its signature is testimony; a FIDO2 key that needs a touch, or signing on another machine, is out of reach.install-hook --publish-head URL(ADR-0025): opt in once, and every session end publishes the chain head to a remote the credentials on this machine cannot delete from (a chat incoming webhook, a retention-locked bucket), after the tail commitment and before the anchor, quietly and best-effort inside the same session-end budget.hook --publish URLcarries it; a re-run of the installer without the flag turns it off;uninstall-hookremoves it. What leaves ishead,n,session,ts,event, and one readable line under bothtextandcontent; never a path, a project name, an action line, or chain bytes. Codex takes the flag as well, with the POST cut off at half its three-second SessionEnd cap (#183). The installer refuses anything but a plain http or https URL with nothing a shell could act on, since the wired command runs through a shell; the three-second cap covers name lookup; a redirect is a failure.install-hook --codex --publish-head URL(ADR-0025 ruling 3, #183): the published head is wired for Codex, whose SessionEnd hook is capped at three seconds. Measured before wiring, on the author's one machine, and the numbers are indocs/HOOK.md: sealing a 2 MB transcript costs 0.20s, the POST 0.06s over it, and a real webhook's handshake under a tenth of a second, while an unreachable remote that silently drops the request cost the full three-second wait and landed past the cap. So a Codex hook waits 1.5 seconds for its POST instead of three, half the cap, which left the worst failure path near 1.8s, and what it cuts off the keeper publishes later. The seal is not bounded, so a very large transcript narrows that margin:docs/HOOK.mdcarries the sizes. The session-end anchor stays refused for Codex: that is a calendar round trip, not one POST.supervisor scan|serve --publish-every AGE --publish-url URL(ADR-0025 ruling 3, #182): the keeper's half of the published head, for machines that run around the clock and for sessions that never reached their end. On each tick, a chain whose head has aged past the cadence and has not l...
v0.2.0
The release after the measurement: what the orientation-cost run (EXPERIMENTS §6) found is fixed or stated, the window between anchors is closed for anyone who opts in at install, and the front door says how the repo is built.
Changed
- The coverage claim states its edge: what the harness does on a session's behalf outside any tool call (a worktree it merges when a session leaves it) fires no hook and leaves no receipt; HOOK.md, the GLOSSARY, and the README's defends-against table say so, and point at git's reflog for the pointer move (#156).
- One session, one drawer (ADR-0023): every receipt of a session goes to the drawer its first receipt chose, so a project directory that resolves differently mid-session can no longer split a session's chain in two. Store-routed writes only;
--log-dirand the cwd-local default are untouched. - The digest header says what it leaves out: when a chain holds bookkeeping entries (transcript commitments), the header adds
plus N bookkeeping entries not rendered (last n M), so a row's sequence number never reads as a missing receipt (#154).
Added
install-hook --anchor-at-session-end(ADR-0024): opt in once, and every session end anchors the chain head to Bitcoin through OpenTimestamps, after the tail commitment, quietly and best-effort under a fixed budget, then spends the leftover budget upgrading the drawer's pending proofs.hook --anchorandhook --calendarcarry it. Nothing leaves the machine without the opt-in; theghhead ledger is rejected as writer-reachable.supervisor verify ADDRESS: the recorder's verdict on the chain holding an entry address, printed verbatim with its exit code, the CLI twin of the MCP tool (ADR-0019, one-to-one restored).shownames the chain's full path and the verify command; the digest footer names it too, so an agent holding an address never has to hunt for the chain file (#155).- A repository's recall (
digest,search,timeline) also reads the drawers of its own harness worktrees (<repo>/.claude/worktrees/), so a session split before this release is shown whole.
Fixed
- A worktree the harness deregistered under a running session still logs to its repository's drawer: the
.gitfile names the repository even after<main>/.git/worktrees/<name>is gone. - A long action line is cut between words, never inside one, and never between a letter and its accent or inside an emoji sequence; a run with no space near the limit is still cut at the limit (#157).
v0.1.0
The first tagged release, cut from the promotion that lands the presentation arc. The tool is versioned from here; the history before this tag lives in adrs/ and docs/HISTORY.md (ADR-0022).
Added
--versionon bothloxodonta.pyandsupervisor.py: tool version, format version, and the checkout's commit on one line.- Releases: a pushed
v*tag publishes both files andSHA256SUMS, with this file's matching section as the notes. - The house checker,
tools/house_check.py: the GLOSSARY anti-terms and the front-door rules enforced by one stdlib script, locally and in CI; markdownlint and a weekly link check as Actions. - The front-door pin test: the README's tamper demo, quick start, and bad-day commands run in the suite on all three platforms, and the verdicts they print are checked.
- The demo store builder,
tools/demo_store.py: a deterministic multi-session store under a neutral home, the only source for screenshots and README excerpts. A bad-day session ships asdocs/demo/bad-day-session.jsonl, byte-checked against a fresh build. SECURITY.md,CODE_OF_CONDUCT.md, a pull request template, anddocs/HISTORY.mdfor the stages before this tag.- EXPERIMENTS §6: the orientation-cost measurement on a second repo, pre-registered, run, and scored; the memory reason on the front door says only what it supports.
- The dashboard screenshot, the tamper demo as a GIF with its tape, the wordmark, and a social preview image, every one from the demo store or neutral ground.
Changed
- The README, rebuilt from the positioning brief and reworked with the author until it read as a story: the receipt sentence, the attack, the bad day first, the tamper demo, Install, an Operator quick start, a bad-day walk, two reading paths.
- The recorder honors
SOURCE_DATE_EPOCHfor the receipt timestamp, so the demo store writes byte-identical chains; a timestamp is testimony either way (ADR-0002). - CONTRIBUTING: the one local check command, the voice rule, the release ritual. CLAUDE.md cut to a map, GLOSSARY given an entry-point preamble, the legacy root
receipts/folder removed.