Repository navigation
v1.24.0
Downloads
| Architecture | MacOS | Linux | Windows |
|---|---|---|---|
| x86 | Mac Intel | Linux x86 | Win x86 |
| ARM | Mac ARM | Linux ARM | Win ARM |
This release includes the commit-level changes below.
Commits
- Format: wrap the rebranded legacy-dirname arrays for prettier: The Prime-Silo rebrand pushed DESKTOP_LEGACY_USER_DATA_DIRNAMES and DESKTOP_UPDATER_CACHE_DIRNAMES past the print width. Prettier's check failed on both files; wrap them as multi-line arrays. No behaviour change. Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- Make authorisation an event, and give the CAS a streaming path: Two gaps blocked the estate control plane, both in the coordination layer. 1. A signature was never recorded. The execution register could show what ran — commit, model, cost, outcome — but not who authorised it, because approval lived in a UI state rather than in the log. After the fact, an unauthorised run and an approved one were byte-identical. governance.mjs makes signing a KEL event, so ADR-001's determinism boundary becomes checkable rather than conventional: a run is authorised iff a proposal_signed event with authorship "human" exists for its proposal (SS1/23 Principle 2). The invariant is enforced in one place — proposalSignedEvent hard-codes authorship rather than accepting it, because a builder that took it as a parameter would put agent self-authorisation one keyword away. isAuthorised checks who signed, not merely that a signature-shaped event exists, so a hand-forged frontier-authored event still authorises nothing. Declines are recorded too: dropping them would make the register look like everything proposed had been approved. Adds proposal_raised/signed/declined to the envelope type enum. Additive — existing events still validate. 2. casStore hashes through a single Buffer, which is right for a session transcript and wrong for the objects the artifact ledger must hold (a 598 MB runtime bundle, a 123 MB installer) on a 16 GB laptop also running Neo4j and a model server. casStoreStream hashes and writes in one pass to a temp file, verifies, then renames — atomic within a volume, so a partial or interrupted transfer can never be observed as a valid blob, and a corrupt or substituted download never enters the store. hasBlob answers the de-dup question without reading the object, which is what makes "acquire once, place many" cheap. Verified against the real 1.23.0 installer: 123.3 MB stored at 55 MB peak RSS, re-acquisition deduped with zero bytes written, no temp files left. casStore, stageSession and every existing signature are unchanged. Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- Artifact ledger: acquire once, place anywhere: The CAS already de-duped by construction, so the same installer fetched on two machines was only ever STORED once. The transfer still happened twice, and on a slow or metered link that is the cost that actually hurts. De-dup after the fact is not the feature; not opening the source is. acquireArtifact takes the publisher's hash — our releases ship .sha256 sidecars and sha512 in the metadata YAML — and when those bytes are already held it never opens the source at all. Verified against the real 1.23.0 installer already in F:\estate-store: source opened 0 times, 123.3 MB not transferred. The grain is a placement: a claim that a named machine holds this blob at this path. Placements are events, so "which nodes have this, and where?" is a fold over the log instead of a walk across three machines. Eviction retires a placement and never the blob — reclaiming space on a laptop must not be able to destroy the only copy. Two gauges rather than one, because the interesting distinction is between them: transferAvoided (deduped, never fetched) is what the store is for; duplicateSpend (deduped but fetched anyway) is waste that should trend to zero. Recording only
dedupedcould not tell them apart. Also hardens placeArtifact: on Windows a drive or share root already exists but mkdir on it raises EPERM rather than EEXIST, so an unguarded recursive mkdir made "place it at the root of the NAS" fail for an unrelated reason. Known and deliberate: the dedup path does not emit artifact_verified, because hasBlob proves the filename and not the bytes. A blob corrupted in place would still satisfy it, so claiming verification there would be a lie the ledger keeps telling. Periodic re-hashing belongs with the GC/scrub work. Adds artifact_acquired/verified/placed/evicted to the envelope enum. Additive. Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Estate heartbeat: notice silence, and only record what changed: On 2026-09-04 the t480's Benny stopped and nobody found out for about twelve hours. Every tier reported correctly the whole time — there was simply nothing asking. The estate could describe itself in detail and could not notice its own absence. Two decisions carry this module. The ledger records TRANSITIONS, not observations. Polling three nodes every sixty seconds is ~86,000 events a day; writing those into an append-only hash-chained log buries the events that mean something under the ones that mean "still fine", and makes the chain expensive to verify. Polling stays cheap and constant; the log only grows when reality changes. Verified live: a second sweep of an unchanged estate writes zero events. Loopback services are observed by the node itself, never across the tailnet. A live sweep found t480/benny and t480/neo4j reporting DOWN over Tailscale while both were up on localhost — they bind loopback by deliberate default (.env: set HOST=0.0.0.0 "only to deliberately expose"). A monitor that cries wolf on its two most important services teaches you to ignore it, and the fix is emphatically not to expose them.
scopesplits the two cases: "local" services are probed via 127.0.0.1 by their own node and attributed with observed_by; only services that genuinely bind all interfaces — memoray, jellyfin — are probed cross-node. The gauge is duration, not state. "benny is down" is a fact; "benny has been down for eleven hours" is what gets attention, and it is only possible becausesincemoves when the state moves rather than when we last looked. A test replays the real incident and asserts longestSilence reads ~12h. Probes are injected so the suite touches no network. Adds node_observed and service_transitioned to the envelope enum (additive). Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Wire the heartbeat to a schedule, and give it its own liveness: The module could notice silence; nothing ran it, which is the same gap in a smaller box. scripts/heartbeat_run.mjs is one sweep — load prior state, probe, record only transitions, persist — designed to be uninteresting most of the time and safe to run every five minutes forever. Per-machine logs, deliberately. The KEL is a hash chain:
previs the hash of the previous line, so two nodes appending to one file over a share would interleave and corrupt it. Each node owns eventlog/heartbeat-.jsonl and health folds over all of them. The watchdog needs watching. A heartbeat that has STOPPED RUNNING produces no transitions, no outages and a clean board — indistinguishable from a healthy estate, which is precisely the failure that started this. So every sweep stamps last_run, isHeartbeatStale turns "no news" into "no news since 09:14", and a capped human trail gives the scheduler a liveness signal of its own (the events file stays silent by design, so without the trail a dead task and a quiet estate leave identical evidence). Live on the t480: Task Scheduler runs it every 5 minutes, Last Result 0, and a second sweep of an unchanged estate writes zero events. The .cmd wrapper needs CRLF — written with LF, cmd.exe mis-tokenises REM and reports "'M' is not recognized". It also uses absolute paths for node and the script and sets its own CWD, because a scheduled task inherits neither a useful PATH nor a working directory. Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Merge the heartbeat logs into one estate board: Each node writes its own hash-chained log, which is not a limitation to work around:
previs the hash of the previous line, so concatenating two chains breaks every link from the join onward. Merging is therefore a READ-side fold — the files stay separate and independently verifiable, and only the view is combined. Order comes from the HLC. Three machines' wall clocks disagree, and2026-09-05T13:44:19.853Z-0000-optimussorts lexicographically into a causally sensible sequence without anyone having to trust anyone else's clock. That is what the field was for. Integrity is reported per source rather than folded away. A board that silently included a tampered log would be a lie delivered with extra confidence, so a failed chain check excludes that log's events and says so. Two things the board refuses to treat as good news, both versions of the failure that started this work: A node whose sweeps have stopped is STALE, not healthy — its log may say "benny up" while nothing has looked since yesterday. A node we could not collect at all is carried through as a blind spot rather than quietly vanishing from the report. Absence reading as health is the subtlest form of the original bug, and dropping the row would reproduce it exactly. Also fixes the pull spec parser: it split host from path on the LAST colon, which lands mid-drive on a Windows remote root (C:/estate-store) and tried to ssh to "…86:C". Verified live: 7 services across t480, optimus and homeassistant in one view, with observed_by showing t480/benny self-observed over loopback while t480/memoray came via optimus. Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Evidence pack: hand a reviewer the proof, including what we cannot prove: Everything else in the coordination layer produces evidence. This is the thing that hands it over — one pack, generated from the logs on disk, readable without a tour of the codebase. The decision that matters is about honesty. Of the four closure defects in the traceability matrix, two are computable from the ledgers today (unauthorised runs, broken chains) and two are not: nothing yet enumerates deliverables or state changes to compare against. A pack that quietly dropped the unmeasured pair would show all green — which is precisely the false assurance an audit exists to catch. So every defect reports one of three states — clean, defects found, or NOT MEASURABLE — and the third may never masquerade as the first.
verdict.completerequires every defect to be BOTH measured AND zero, so the script exits non-zero while any gap remains. The load-bearing test asserts what the pack refuses to say. Chains are verified per file and a broken one is excluded from the fold and named, because a merged report that silently includes a tampered log is a lie delivered with extra confidence. Signatures are listed with signer and timestamp rather than counted, since a count is not evidence. Live against the estate: 19 events over 3 ledgers, all chains verify, 123.3 MB transfer avoided, 7 services tracked, no outages — and the pack still reports NOT COMPLETE, correctly, because three defects remain unmeasurable. Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Collector: persist the board so a scheduled run leaves evidence: The estate board printed to stdout only, which is fine when a person is watching and useless on a schedule — a collector whose output vanishes leaves nothing behind when nobody is looking, which is the same "no evidence" problem the heartbeat exists to solve. --out writes the rendered board, --quiet suppresses the console copy. Now scheduled on the t480 every 15 minutes (the per-node sweeps stay at 5; the merged picture does not need to be fresher than that, and it keeps SSH chatter down). Exit 1 means outages or blind spots were found — data, not a task failure. This closes the loop the governance layer was built for: the change was proposed by an agent (authorship frontier), signed by a human, and only then executed. Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- Make unauthorised-run measurable, against a control effective date: "Unauthorised runs: 0" means nothing without a list of runs to check it against. runs.mjs assembles that denominator from execution evidence already on disk — Benny RunRecords and the LONGVIEW OpenLineage feed — so the count is over real executions rather than a promise. Twenty runs found. The part that makes the number honest is the effective date. Every run in this estate predates the governance layer, because signing did not exist until it was built; failing a July run for lacking a September mechanism reports 100% unauthorised, which is true, useless, and precisely the wall of red that teaches an operator to ignore a gauge. Controls have effective dates for this reason. So the epoch — the earliest proposal event, or an explicit override — splits the population, and BOTH halves are reported. Pre-control runs are excluded from the defect and shown anyway, so the exclusion is visible rather than silent. In-scope runs must carry a human signature or they are named individually. An undated run is treated as IN scope. Defaulting an unknown timestamp to "pre-control" would let anything escape the check simply by not recording when it ran, and an undateable execution is a governance problem in its own right. Unmeasured defects drop from 3 to 2. The pack still reports NOT COMPLETE, because orphan-artifact and unrecorded-action need external inventories that do not exist yet — and a green verdict it has not earned is the failure this pack exists to avoid. Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- Gov arc: sign in the app, not on someone's behalf: The signing queue existed as a library and a set of scripts, which meant the only way to authorise anything was to ask an agent to do it for you. That is a strange shape for a control whose entire point is that a human decides. Two rules live in gov_sign.js and nowhere else: THE CLIENT CANNOT CHOOSE WHO SIGNED. The signer comes from the authenticated session, or failing that the OS account the server runs as; a signer in the request body is ignored outright. A signature you can type another name into is a text field, and the register would inherit that weakness everywhere. ONLY A HUMAN SIGNS. authorship "human" is hard-coded by proposalSignedEvent — never passed from a browser. An agent may raise and may never authorise (ADR-001 / R38). The endpoint also refuses to append onto a ledger whose chain no longer verifies: a signature written after a break is evidence resting on a record we already know was altered. The UI surfaces that state rather than rendering tampered rows as if they were fine, and disables signing while it holds. Declines are recorded and shown beside approvals. A register that kept only approvals would imply everything proposed was accepted, which is a flattering lie about how decisions actually went. Follows the experience contract: one primary action (Sign) with Decline quiet beside it, evidence as disclosure rather than a third competing button, why/cost/ reversibility above the controls so the decision precedes the button, Benny narrating in the first person, and an empty state that offers the next thing instead of documentation. C0 gate green — every colour resolves through a token, no hex in governed CSS. Motion only as feedback to the user's own click, gated on prefers-reduced-motion and the zen profile. Verified against the live ledger: reads F:\estate-store, chain verifies, and the refusal paths return 400/404/409 as intended. Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- Gov arc: fix three faults the live page exposed: Opening the page in the running shell found what the unit tests could not. 1. The framework tokens were never loaded. DESIGN-SYSTEM.md names layout.css and type.css as the source of spacing, radius, measure and type, but nothing in the shell loads them — framework/css/index.css imports only google-icons, and no module links them. Colours arrive via colors.css, so the page LOOKED right while every var(--space-) and var(--font-size-) was undefined and silently computing to 0 or inherited: container padding rendered 0px and body text 15px, below the 16px dyslexia rule the contract sets. Linking both here fixes the surface — padding 24px, font 16px, line-height 24px — and surfaces a gap the design system has carried unnoticed. 2. A missing ledger read as an empty queue. The resolver guessed a path that did not exist and the screen cheerfully reported "Nothing is waiting on you" — an absent record presented as an all-clear, which is the most dangerous sentence a governance surface can say. governanceLogPath now reports
exists, and the UI gives the missing case its own words rather than borrowing the calm ones. 3. The store resolver guessed badly. With no ESTATE_STORE set it derived a sibling of BENNY_HOME and stopped there. It now tries the plausible roots and PREFERS ONE THAT EXISTS, so an unconfigured install lands on the real store instead of an empty imaginary one. Verified live in the shell at 1280x800 and 375x812: reads F:\estate-store\eventlog\governance.jsonl, chain verifies, and the signed collector proposal renders under "Already decided" with signer and timestamp. No horizontal overflow at 375px. C0 gate green. Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Retire the dead upstream name from the launcher, login and admin pages: The experience contract is explicit: "Space Agent" is a dead upstream name and must never appear on a user-facing surface. It was still on three of them — thirty times — in tab titles, application-name and apple-mobile-web-app-title meta, Open Graph and Twitter card titles, image alt text, and the aria-labels screen readers announce. The first thing the product said about itself, on the very first screen, was the wrong name. Strings only. Identifiers stay exactly as they were, per the same contract: space-agent, space-desktop and space.enter survive untouched (16, 1 and 2 occurrences), because renaming those is Workstream H and not this change. The diff is 30 insertions and 30 deletions with no structural edits. Verified served: /enter is "Enter Space | Prime-Silo", /admin is "Admin Mode | Prime-Silo", and no page returns the string "Space Agent" at all. NOT changed, deliberately — these are destinations rather than branding, and repointing them is a decision rather than a cleanup: Social preview meta still points at https://space-agent.ai/ and its banner, so a shared link previews the upstream site. Fixing it needs a destination we own. The download and host links still point at github.com/agent0ai/space-agent, including /releases/latest. That one is the same class of bug as the updater feed fixed in 80b5f70: it sends people to the upstream project's releases rather than ours. Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- chore(brand): repoint page links off the upstream domain to Prime-Silo: The three served pages still carried space-agent.ai and agent0ai/space-agent in og/twitter metadata and footer links, so anything shared from the app previewed as somebody else's product. Destinations were verified reachable before the swap, not assumed: site https://binary16labs.github.io/prime-silo/ 200 og-image .../prime-silo/og-image.png 200 image/png 413556 B repo https://github.com/binary16labs/prime-silo 200 releases .../prime-silo/releases/latest 200 The upstream banner (pages/res/readme-banner.webp) maps to our og-image.png; the "#host" anchor was dropped rather than carried over, because no such heading exists in the Prime-Silo README and a link to a fragment that isn't there is a link that silently lands in the wrong place. Left alone deliberately, for the owner to decide: the Discord/X/"Agent Zero" community links (I have no way to know which socials are ours) and the "Browser-First AI Agent Runtime" tagline, which disagrees with the live site's "Sovereign Local-First AI Agent OS". Zero upstream references remain in the three files; prettier clean. Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- chore(brand): drop the upstream community links and align page metadata to the site: Removes the Discord and X links (both pointed at the upstream project's communities, not ours) and deletes the two icon assets, whose only consumer was those links. The whole
- goes each time, so no empty list item is left in the flex row; GitHub and the Agent Zero credit link remain. Metadata now matches what the live site actually says about itself, rather than a third wording that exists nowhere: title Prime-Silo | Browser-First AI Agent Runtime -> Prime-Silo — Sovereign Local-First AI Agent OS description the site's own long-form line og/twitter the site's own short line ("Own your AI memory. ...") Worth recording: "browser-first" was never the contradiction it looked like — the site's own uses that exact phrase. Only the TITLE disagreed, so the description keeps the wording and the title is what changed. Also corrects og:image metadata the previous commit carried over unexamined. Those tags still described the upstream banner: 1774x887, image/webp. The asset we now point at is 1024x1024, and its bytes are JFIF — a JPEG behind a .png name, served as image/png. Width and height are corrected; og:image:type is dropped rather than re-asserted, because all three candidate values (webp, png, jpeg) disagree with at least one of the extension, the served header, or the bytes, and a type hint a scraper trusts and finds false is worse than no hint. Two things the owner may want to fix at the source, in the website repo: the og-image is a square, which twitter:card=summary_large_image will crop to 2:1, and the file should be named for what it contains. C0 gate green; prettier reports all three files unchanged. Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- Lineage arc: fold the whole estate by subject, and measure what is missing: The read side of SS1/23 Principle 1. Every ledger in the estate store is folded by subject id into one index, and any subject can be walked event by event with the projection the system would have returned at each step. The event envelope already carried everything lineage needs — subject.id, both times, machine, authorship, and a per-file hash chain — so this adds no new writer. It is a fold, not a schema. What the design refuses to do, and why: Chains stay per-ledger. heartbeat-t480 and governance are separate chains and cannot be concatenated, so each is verified alone. A break quarantines only its own ledger's subjects: they are still shown, because hiding them would lose the fact that they exist, but they never count toward attestation. Provenance edges are declared, never inferred. An edge exists only where an event carries derived_from or caused_by. Deriving edges from name similarity or timestamp proximity would manufacture the exact audit trail the regulation exists to test, and a manufactured trail is indistinguishable from a real one. An empty result is not a clean result. Zero broken chains across zero ledgers is an absent record, and a subject with no upstream is unknown rather than original. Both render as measured gaps, in the evidence pack's vocabulary. Against the live store (F:\estate-store) that yields: 4 ledgers, all verifying; 12 subjects, all attested; and ORIGIN RECORDED 0 / 12. That zero is the honest headline — the estate has integrity but no genealogy, because nothing yet writes derived_from. Ledger-to-world completeness stays NOT MEASURABLE for the same reason it does in the evidence pack: no external inventory exists to compare to. Two things the build surfaced, both fixed here rather than papered over: foldProjection is last-write-wins per subject, not an accumulation — folding a signed proposal returns {signer, note} with the title gone. The field is named
projection, notstate, and the view says so under every step, because calling it state invites the reader to think earlier facts were retracted. The subject rows reached the accessibility tree unnamed, so each now carries an explicit aria-label with the full sid, plus aria-current for the selection. Verified live: index/trail/404/400 all correct; step-through walks a real proposal from raised (frontier) to signed (human) with Prev disabling at step 1; type tokens resolve (16px / 24px line-height); no sideways overflow; and zero animations run inside the pane at rest — the two on the page are Benny's sanctioned mascot micro-states. C0 gate green, prettier clean. The panel rides the Gov arc colour on purpose: it is the evidence side of the arc Gov decides on, and navi-key.js has ten fixed arcs — an eleventh reflows the whole ring, which is a navigation change to make deliberately, not in passing. Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Provenance writers: let an event say where it came from, in one validated place: Lineage reads genealogy from exactly two payload fields, derived_from and caused_by, and until now nothing wrote either — so ORIGIN RECORDED sat at 0/12 against a fully verifying estate. This adds the writing half. provenance.mjs is the single place those fields are constructed, for the same reason proposalSignedEvent hard-codes authorship: a field each module formats for itself drifts, and a drifted edge is silently unresolvable. Three rules, each because the alternative reads as evidence: An edge must be a subject id, never prose. "because the collector was manual" is a reason, not a reference; written into derived_from it becomes an edge that can never resolve and shows up as a dangling parent forever — a permanent defect no later instrumentation can clear. Reasons stay in rationale/note, which are untouched. Nothing is its own ancestor. A self-edge makes a subject look provenanced while saying nothing. Absent is not empty. With no provenance the keys are OMITTED, never written as [] — an empty array asserts "derived from nothing", a positive claim of originality. Omission says "not recorded", which is the truth, and is what lets lineage separate unprovenanced from linked honestly. derived_from is a list (a thing may be made from many); caused_by is single (one decision brought it about). A set of maybe-causes is a guess wearing a fact's clothes. Wired into the builders AND their orchestrators — acquireArtifact (both the deduped and the fetched path), placeArtifact (including already-there, where the claim "this machine holds this on your say-so" is still true), evictPlacement, and proposalRaisedEvent. Threading the orchestrators is the point: a builder that accepts provenance while its caller drops it is the same defect as the gauge that read a
fetchedflag nobody wrote. Not wired, deliberately: heartbeat observations. A service going down is not caused by any decision we hold, and inventing that edge is exactly what the first rule above forbids. artifact_verified is likewise skipped — its cause is the acquisition, which shares its subject, so there is no edge to draw. Verified end to end on a scratch store: 12 checks covering the four rejection cases, de-duplication, omission-when-absent, both builders, the orchestrator threading, and the fold — where ORIGIN RECORDED moves off zero, children appear on the parent, and an edge to a subject we hold no events for is reported as dangling rather than dropped. Existing artifact_ledger_test suite still 9/9, evidence pack unchanged, C0 green. The live estate is untouched and still reads 0/12: these writers have no committed caller yet — the artifact events in F:\estate-store were written ad hoc. The number moves when a real acquisition or a raised proposal goes through these paths; I have not backfilled edges into the existing ledger, because inventing history is the failure this whole design exists to prevent. Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Gov raise path: proposals get a real origin, and a signature stops being erasable: Signing was the only writer on the governance ledger, so the queue could only be seeded by hand and every caused_by edge in the estate had nothing legitimate to point at. POST /api/gov_raise is the generating end. Raising is deliberately not the mirror of signing, because the two carry different power. A signature authorises; a proposal only asks: Authorship is "frontier", hard-coded. Everything reaching an HTTP endpoint is a program making a request. A human's intent enters this ledger through a SIGNATURE, which is verified and attributed — calling a request "human" because someone was logged in when a program sent it is the same category error as letting a client name its own signer. A human recorded as frontier loses nothing; their signature is still required. An agent recorded as human would be a lie with consequences. Who asked is kept as
requested_by. Raising is idempotent. An identical re-raise writes NOTHING: the fold collapses duplicates into one queue item either way, so the copy is invisible in the UI while the ledger grows without bound, burying real history in restatement. A changed re-raise refreshes the open proposal in place. A proposal without a rationale is refused. The signer decides on the why, and "the agent suggested it" is not a why. derivedFrom is validated by provenance.mjs, so prose is a 400 at the door rather than a dangling parent in the lineage graph forever. Approval fatigue — not a technical fault — is the realistic way a register like this dies. Every rule above exists to keep the queue small enough that signing still means something. Found and fixed while building it, in buildGovernance: a secondraisedevent for an already-signed proposal SILENTLY UN-SIGNED it. The fold rebuilt the record with state "open" and signature null, while isAuthorised(), which scans raw events, went on reporting it authorised. The two disagreed and the visible one was wrong — the queue would ask for a signature already given, and you would sign twice for the same work. A settled proposal is now terminal against re-raising exactly as it is against a second signature, and the re-raise is counted rather than dropped. An endpoint that makes re-raising easy would have turned that latent bug into a routine one, so it could not ship first. Verified: 21 checks on a throwaway store covering every rejection (bad slug, no title, no rationale, prose edge), that no rejection writes, first raise 201 with frontier authorship and the requester recorded separately, identical re-raise appending nothing, changed re-raise refreshing to one queue item, 409 after signing with nothing appended, the fold and isAuthorised agreeing, and the derived_from edge arriving in lineage with the chain still verifying. Live: routing and all three validation paths correct against the running app, with the real ledger untouched at 2 lines. governance_signature_test 9/9, artifact_ledger_test 9/9, evidence pack unchanged, C0 green. No proposal was raised into the real estate ledger — the first one should be yours to choose. Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Artifact CLI: download once, place anywhere, and prove who said so: The first committed caller of the provenance writers. derived_from and caused_by existed and nothing invoked them, so ORIGIN RECORDED sat at zero against a fully verifying estate; this is the entry point that can move it. Commands: acquire (url or local path), place, evict, ls. The saving is real and tested — with --expected-hash and the blob already held, the source is never opened at all, so a second machine costs nothing rather than another 123 MB. The invariant that makes the edge worth having: A --caused-by naming a proposal must name one a HUMAN ACTUALLY SIGNED. The id is resolved against the governance ledger and refused if it is missing, still open, or declined. An unchecked authorisation field is worse than an empty one: it puts the shape of approval into the record for something nobody approved, and afterwards it is indistinguishable from the real thing. Causes that are not proposals have no signature to check and are recorded as given, which the command says out loud rather than implying a check it did not do. Omitting --caused-by stays legal, deliberately. Forcing an authorisation on every fetch would teach the operator to type a plausible proposal id to get past the tool, and fabricated provenance is the exact failure this design exists to prevent. An unauthorised placement is recorded honestly instead, and the lineage fold counts it unprovenanced. It also refuses to cite any proposal at all when the governance chain does not verify — an authorisation read out of a record we know has been altered is not an authorisation. The subcommand comes first, git-style: accepting it after flags would need the parser to know which flags take values, and
--json acquireis genuinely ambiguous. Getting it wrong prints the corrected command rather than just refusing. Verified: 10 checks on a throwaway store — prose and non-existent proposals refused with nothing written, an OPEN proposal refused as an authorisation, the same command accepted once signed, an unreachable source short-circuited by a held blob, place/ls/evict with the blob surviving eviction, and the artifact arriving in lineage with the signed proposal as its parent (linked 1). Against the real store, ls reads correctly (1 artifact, 2 placements) and both ledgers are untouched. artifact_ledger_test 9/9, C0 green. The live estate still reads ORIGIN RECORDED 0/12. The only signed proposal there authorises a collector schedule, not an artifact, and citing it for a placement would be precisely the false authorisation this command refuses. Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Inventory sweep: measure the last two defects, and stop an empty gauge reading as a pass: orphan-artifact and unrecorded-action have been NOT MEASURABLE since the evidence pack was written, for the same reason: you cannot count what is outside a ledger by reading the ledger. This is the outside look, and it closes both. orphan-artifact — bytes on disk that no event accounts for (orphan blobs, files under a swept root with no live placement). unrecorded-action — a placement the ledger asserts whose file is gone. The absence IS the evidence, and it is the only form an unrecorded action can take that an inside-out system can honestly detect. The load-bearing decision is scope, because it is what stops this becoming the dishonesty it exists to remove. Reconciliation is keyed on PATH, not machine: a placement is judged if and only if its path lies under a root actually walked. The store is on a share, so a t480 sweep legitimately reconciles optimus's placements under F:\ — while a path on another node's C:\ is neither present nor missing, it is UNSEEN, counted separately and folded into neither verdict. Marking an unreachable path "missing" manufactures a defect out of not looking; marking it "fine" hides a real one. Only the third answer is true. The scope travels into the result and into the pack, because "0 orphans" means nothing without the boundary it was measured in. Observation and reconciliation are separate functions on purpose: a snapshot is a fact about the world, a comparison is an opinion about the ledger, and keeping them apart lets a stored snapshot be re-reconciled later without re-walking a disk. --record stores that snapshot in the CAS and appends a sweep_recorded event pointing at its hash — a finding whose observation cannot be retrieved is an assertion. The sweep also refuses to reconcile against a ledger that does not verify: the claims being checked may themselves have been edited. Then the pack said "Complete. Every closure defect is measured and zero" — and that was a new overclaim of exactly the kind this design exists to prevent. unauthorised-run reports zero because ZERO RUNS ARE IN SCOPE, not because runs were checked and passed; 0 of 0 and 0 of 200 are the same digit and completely different assurances. So every defect now carries
population, the table has an "Of" column that flags an empty one, the verdict names any vacuous gauge, and onlysubstantiated— measured, zero, and non-empty everywhere — is an unqualified pass. The exit code follows the stricter verdict. Against the live estate: 1 blob, 2 files, 2 placements claimed and all reconciled, zero orphans, zero unrecorded actions, nothing out of reach. The pack now reads "Measured and zero — but 1 of 4 gauges had nothing to measure: unauthorised-run", and exits 1. That is the honest state, and it is a better result than the "Complete" it briefly printed. Verified by injection, since a clean result on a clean estate proves nothing about detection: 12 checks covering a baseline, an orphan blob, an orphan file, an unrecorded deletion, the control that a RECORDED eviction is not a defect, a blob the ledger claims but does not hold, an unreachable placement reported unseen and NOT as missing, --include bringing it into scope, and an uppercase forward-slashed path still matching the file on disk. artifact_ledger_test 9/9, governance_signature_test 9/9, C0 green. Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Make unauthorised-run substantive: record executions, and let the gauge find them: The last vacuous gauge. unauthorised-run reported 0 of 0 because nothing wrote runs into the ledger, so "no unauthorised runs" was a statement about an empty set. Runs are now recorded, and the pack has something to check. The design rule here is the OPPOSITE of the artifact CLI's, and the inversion is the point. That command refuses to cite a proposal nobody signed, because a citation of authority never granted is a false claim. A run record is not a citation — it is an observation that something executed: A SYSTEM THAT WILL NOT RECORD AN UNAUTHORISED RUN CANNOT DETECT ONE. So runRecordedEvent takes proposal_id exactly as claimed, including null, and refuses nothing. Verification lives in the read, where buildEvidencePack resolves each claim through isAuthorised(). Validating at write time would guarantee every recorded run passes and the gauge would report zero for ever while meaning nothing. Producers wired: the artifact CLI records every acquire/place/evict, and the heartbeat collector takes --run-proposal so each scheduled firing is recorded — that one matters because it makes the population grow on its own rather than only when someone types a command. Its outcome carries what the collection actually found; a register holding only successes answers "did it run?" with "did it run well?", which hides exactly the executions an auditor came for. Two bugs this surfaced, both mine: run_recorded and sweep_recorded were not in the envelope's type enum, which is a closed vocabulary. The sweep's --record path shipped last commit was therefore broken and I had never exercised it. sweep_recorded is now added; runs reuse
execution_recorded, which was already reserved for the L5 execution register and never written — a second word for one concept is worse than reusing the reserved one. The first --record sweep stored its own snapshot in the CAS and then reported that snapshot as an orphan artifact: the audit contaminating what it audits. The orphan rule keyed on artifact_acquired, but "accounted for" means SOME event references the bytes, and sweep_recorded names the hash. Orphans are now judged against every hash referenced anywhere in the ledger. Missing blobs stay keyed on acquisition, because only an acquisition claims we hold bytes. Verified: 15 checks on a throwaway store — runs citing declined, still-open, non-existent and absent proposals are all recorded, then all four are caught, the signed one is not, and the gauge goes clean without going vacuous once the estate is right. Detection suite still 12/12, artifact CLI 10/10, both unit suites 9/9, C0 green. Live: one real collector run recorded under the signed proposal:collector-schedule. The pack now reads Complete — every closure defect measured, zero, and with something to measure, exit 0, for the first time. Populations are 1 / 4 / 2 / 6, so it is a true statement about a genuinely small estate, not a broad assurance. ORIGIN RECORDED moved 0/12 to 1/14. Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Operating manual: one source, four consumptions, and a build that refuses to lie: A manual written separately from the code is wrong within a month, and wrong in the worst way — confidently specific about something that no longer exists. So manual/manual.json is the only file anyone edits and everything else is generated: the tour page, the markdown read, retrievable chunks for the agent, and graph triples for the knowledge graph. What makes it trustworthy is not the generation, it is the CHECK. Every file a feature cites, every panel surface it names, every API it advertises and every command it prints is resolved against the repository, and the build FAILS rather than describe something that is not there: THE MANUAL CANNOT DESCRIBE SOMETHING THAT IS NOT THERE. Generating from a file is only reproducibility; refusing to generate when it has drifted is correctness. --check asserts it in CI without writing. The gate proved itself twice already: on its first run it caught me documenting app_onboard.mjs before writing it, and a deliberate three-way break (missing file, unregistered panel, imaginary endpoint) was caught with all three named. app_onboard.mjs is the workflow everything else was built for — propose, decide, acquire, place, remember — and its value is the gate in the middle.
proposewrites a proposal and stops;acquireandplaceresolve it against the governance ledger and refuse while it is open, declined or absent. There is no --force: a step that can be skipped under pressure is not a control. Status is folded from the ledger, never stored, because a stored status drifts and the stored one always looks better than the truth. Onboarded applications become manual features automatically. If "remember" meant "now hand-write an entry", it would be skipped, and the agent's memory would know every feature except the most recent — the exact drift this prevents. manual_load.mjs pushes triples to the graph and ingests the document for retrieval. It caught itself in its own trap during the build: it printed "Loaded" while retrieval had failed on an unreachable embedding host, which is precisely what its header forbids. It now requires BOTH halves and reports partial loads as partial, because an agent answering confidently from a manual it never received is worse than one that says it does not know. Verified: 13 features, 2 workflows, 9 arcs, 23 chunks, 271 triples, every reference resolving. Onboarding gate 10/10 on a throwaway store — nothing fetched before signing, both acquire and place refused while open, status naming exactly where it is stuck, and after signing the artifact and both runs citing the signed proposal. Graph load verified queryable in Neo4j (204 nodes, 271 edges). C0 green. Retrieval is NOT loaded: the embedding host is down, the script says so, and exits 1. Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Manual load: a retrieval-shaped document, and two more places the script lied: The RAG is loaded and answering. Getting there exposed three defects, all in the loader, all of the same family this whole estate is built against — a tool reporting something other than what happened. It printed "PARTLY LOADED" and "Loaded." together. Changing process.exit to process.exitCode (to avoid a libuv teardown assertion) let execution fall through to the success line. Second time this file contradicted its own header; the success line now sits in an else branch where it cannot be reached by accident. It reported "ingest failed: timeout" while the ingest was still running. A client timeout is not a server failure — the work continued and did in fact complete. Claiming something did not happen because we stopped watching is the same lie in the other direction, so a timeout now says the ingest is probably still running and to believe /api/rag/query over this script's own report. The wait is 20 minutes, not 3: Benny serves ingest on the request thread and the whole API stops answering while LM Studio embeds. Retrieval quality was bad and the test showed exactly why. Asking "what does the artifact CLI refuse to do" returned the literal string "What it refuses to do" five times with no content: the splitter cut the headed manual on structure and handed back the labels. manual_build.mjs now also emits manual.rag.md — no headings, no lists, one dense self-contained paragraph per feature and per workflow step, which is the unit we want returned. That is what gets ingested; OPERATING-MANUAL.md stays the human read. LM Studio was already serving with zero models loaded, which surfaces as "embedding provider unreachable". Loaded nomic-ai/text-embedding-nomic-embed- text-v1.5 — deliberately not the cstr/ fork of the same model, which Benny's own embeddings.py documents as 400ing — and probed it (768 dims) before trusting it. Verified: graph 204 nodes / 271 edges; ingest status "completed"; every chunk now returns whole, with its arc and context. "who is allowed to sign a proposal" returns "Decide — performed by you, and only you" first. Manual check passes, C0 green. Ranking is honest but soft: each chunk carries purpose, usage and invariants together, so its embedding averages several topics — good recall, blurry precision. "what does the artifact CLI refuse to do" ranks Run register above the artifact feature. Finer chunks (one per invariant) would sharpen it. Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- Manual: one chunk per invariant, and the document hierarchy Benny already knows how to read: Two changes that turn out to be the same change. FINER CHUNKS. The old grain packed purpose, usage and every invariant into one paragraph, so a chunk's embedding averaged several topics — good recall, blurry precision, and "what does the artifact CLI refuse to do" ranked the wrong feature. An invariant is the unit people actually ask about ("can an agent sign?"), so it is now its own chunk: 23 -> 65. Each still names its feature and arc in its own text; splitting finer makes self-containment more important, not less. THE HIERARCHY, VIA THE PATH THE RUNTIME ALREADY HAS. benny/core/pageindex.py (PIX-001 / ADR-002) is the vectorless ingest: build_tree_from_markdown turns HEADINGS INTO THE TREE with no LLM, writes (:Document)-[:HAS_SECTION]->(:Section) to Neo4j, and anchors triples to a node_id instead of a filename — its own comments cite the legacy chunks[:10] path that silently dropped everything past the tenth paragraph. Most documents need a model to guess their structure. This one does not: manual.json IS the hierarchy, so manual.pageindex.md is emitted with heading levels mirroring it exactly. The structure is transcribed, not inferred, and the finest heading is a single invariant — so "what may an agent never do?" resolves to a Section rather than a paragraph that happens to mention it. That, plus the outline (101 lines, titles and summaries, no bodies) the agent reads first to choose what to open, is the state awareness. extract_triples and use_llm_summaries are both OFF. Each asks a model to invent facts already derived exactly from the manifest; running them would put a second, guessed account of the same facts in the same graph with nothing downstream able to say which to believe. Verified in Neo4j: 78 sections, depths 1/1/9/35/55, and a real path Document -> Manual -> Gov arc -> Raise a proposal -> refusal 1. The generated document has exactly 78 headings, all unique, with no level skips — so no synthetic parents and no node_id collisions. One residual failure, diagnosed rather than hidden. Finer chunks did NOT fix "artifact CLI", because that feature is called "Download once, place anywhere" and its text never contained the words anyone would type. A correct chunk that shares no vocabulary with the question is unreachable at any grain. So features carry
aliases— the words people actually use — into every chunk and Section body. After it: "download an installer once and share it to the NAS" and "can an agent sign its own work" both return the right chunk first; "artifact CLI" rose from absent to tied first, still tied with an unrelated ledger refusal. The next lever is not grain, it is boilerplate: every invariant chunk opens with " ( arc) refuses to do this:", and that shared prefix is now itself a source of similarity competing with the content. Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Strip the shared prefix from embedded chunk text: Every invariant chunk opened with " ( arc) refuses to do this:". Identical on all 42, so it contributed the same similarity to every one of them and could discriminate between none — ballast competing with the words that actually differ. 206 -> 175 chars per chunk. What stays is what varies: the feature name and arc differ per chunk, so they still carry context (the self-containment rule that earlier rounds established is not weakened by this — it is the repeated PHRASING that went, not the context). That a chunk is an invariant moved to
kind, which is metadata's job. Scoped to what gets embedded. manual.rag.jsonl, manual.rag.md and manual.pageindex.md have zero occurrences; OPERATING-MANUAL.md and tour.html still have 13, because there "What it refuses to do" is a heading a person reads, not text a model averages. Measured on the same four queries, before -> after: "artifact CLI refuse to do" tied 1st with the WRONG chunk -> RANK 1 "can an agent sign its own work" rank 1, 28 -> rank 1, 30 "download once and share to NAS" 2 of top 3 correct -> all 3 correct "what may an agent never do" -> rank 1, 35: "may propose and may never authorise (ADR-001)" Absolute scores fell as ranking improved (40 -> 33 on the first). That is the diagnosis confirming itself: the old scores were partly measuring agreement on boilerplate every chunk shared. Honest limit: on that first query the top three now sit at 33, 33, 33. The right chunk sorts first but the margin is nil, so it is not a robust win — unlike "can an agent sign" (30 vs 27) and "what may an agent never do" (35 vs 25), where the gap is real. A query whose only anchor is an alias still has thin signal. Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Fix structured retrieval: branches resolve, sources match leniently, failures announce themselves: Outline routing already existed (retrieve_structured, PIX-F14) and never worked on a deep tree. Three defects, found by driving it against the manual: A SELECTED BRANCH RESOLVED TO NOTHING. Asked "which section covers X" a model names the section — "0.0.5.0", the feature — not the leaf beneath it. Matching only against leaves made that resolve to nothing, and the empty result fell through to flatten_leaves(tree)[:3]: an arbitrary slice of the front matter. The pipeline then reported three documents retrieved and three graded relevant while holding none of the content asked for. Branches now expand to their subtree leaves, deduped and capped at 12. THE SOURCE LABEL WAS TREATED AS A KEY. The selector returned "PRIME-SILO-MANUAL-PAGEINDEX:0.0.5.0.0" while the tree is keyed "...-PAGEINDEX.md", because what it sees in the outline is the root node's TITLE, not the catalogue heading. Every id missed over one absent file extension, and the miss was indistinguishable from the model choosing badly. The source is now a hint, the node_id is the key. THE FALLBACK PASSED FOR A HIT. "Nothing resolved" and "retrieved what you asked for" were the same shape downstream. The fallback still answers — a blank is worse — but logs and tags the trace :unresolved-fallback. Document-side, in manual_build.mjs: feature headings in the machine-read pageindex document now carry their aliases ("Download once, place anywhere — artifact CLI, scripts/artifact.mjs, acquire, ..."). The outline is titles and summaries only, so aliases sitting in the body were invisible to the selector, which answered "artifact CLI" with "Where a thing came from" and "Run register". This is what an index is for. OPERATING-MANUAL.md and the tour page keep the clean name. Verified live, against the patched process (new pid, patched source on disk, and the trace tag as proof the code is the one running): the selector now names the four correct refusal leaves when probed directly, resolution succeeds with no fallback tag, and the answer is grounded. Two things this did NOT fix, stated plainly: Selection is still variable. Probed directly the selector picks all four refusal leaves; through the pipeline it picked the usage leaf instead and answered "the context does not mention what the CLI refuses to do" — correct about what it held, wrong about what it should have held. A 7.5B model over a 12.8KB, 101-node outline is the limit; google/gemma-4-12b is on disk if this matters more than latency. THE PIPELINE FABRICATED A QUOTATION. On the unresolved run it produced a block-quoted "Artifact Generation" section about Git SHAs and immutable commit history — none of which exists anywhere in the manual — and appended its own note admitting the context lacked the text. The trace shows why nothing caught it: build_adaptive_rag_graph(self_check=False) is the default, so check_hallucination and check_answer_quality never run. Not changed here, because enabling it alters behaviour for every query and costs round-trips — but an estate that refuses to let an unverified claim pass for evidence everywhere else should probably not leave that gate off. Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- Turn on self_check: grade every answer, without re-enabling the loop that broke it: The hallucination and answer-quality graders existed and were off, so the pipeline could return a fabricated quotation from an operating manual with nothing between the model and the caller. They are on now. The flag bundled two very different things, and only one was expensive. The CHECK costs one call and says whether an answer is grounded in what was retrieved. The REWRITE LOOP it can trigger re-retrieves and re-generates, and that is what ran for minutes on a local model until the caller's connection dropped. The loop is already governed by max_retries, and after_hallucination_check only rewrites while retries remain — so at max_retries=0 the pipeline detects and labels without ever looping. AdaptiveRAGRequest.max_retries defaulted to 3, not 0. Turning the check on without touching that would have silently restored the exact loop the original docstring warned about. It is 0 now: detection cheap and on, correction expensive and opt-in.
self_checkis exposed on the request so a latency- critical caller can decline it — as a decision someone makes, not a default. The route also sets X-RAG-Grounded: true|false|unchecked, so a caller that reads only headers, or a proxy that logs them, cannot treat an ungrounded answer as a plain 200. hallucination_check and answer_quality were already on the response model; they were simply always null because the nodes never ran. Verified live. The structured query that previously invented an "Artifact Generation" section about Git SHAs now runs the full six-node trace and comes back grounded=false with the matching header. FOUND WHILE VERIFYING, AND NOT FIXED HERE: no_retrieval bypasses grounding entirely. Asked "can an agent sign its own work" — a question this estate answers with "never, only a human signs, ADR-001" — the router chose no_retrieval, never opened the manual, and returned a generic essay on digital signatures and watermarking. check_hallucination is skipped on that path by design (there are no documents to be grounded in), so it arrived hallucination_check=null with answer_quality=true: a wrong answer graded adequate. The X-RAG-Grounded: unchecked header is what makes this visible at all. The real fix is routing — an estate-specific question that merely SOUNDS generic should not be answered from model memory when the workspace has an ingested manual — and that changes behaviour for every query, so it is reported rather than changed unilaterally. Co-Authored-By: Claude Opus 5 noreply@anthropic.com - Bias the router: a workspace with its own documents does not answer from memory: no_retrieval is the one route that never opens the workspace and never gets grounded — check_hallucination is skipped there because there are no documents to check against. So an answer that lands on that route is unverifiable by construction, and it is precisely where a confident wrong answer hides. It was not theoretical. Asked "can an agent sign its own work" — a question this estate answers with a hard rule — the router chose no_retrieval, never opened the manual, and returned a fluent essay on digital signatures and watermarking, with hallucination_check null and answer_quality graded true. Two layers, because prompt wording alone cannot carry this. The model is being asked to know that a plain-sounding question is domain-specific, which is exactly what it cannot know from the question alone: The prompt now reserves no_retrieval for greetings and pleasantries, and says why: in a workspace holding its own documents, "what is a proposal?" is asking what THESE documents say. A deterministic backstop overrides no_retrieval to single_step when the query is not social and the workspace has material of its own. The social set is tiny and literal on purpose. The two errors are not symmetric — retrieving for a greeting wastes one lookup, skipping retrieval on a domain question yields a wrong answer nothing checks — so anything not obviously social gets retrieved. The override is recorded, never silent: it tags the trace smart_router:no_retrieval-overridden and appends its reasoning to route_explanation. A behaviour change invisible in the trace is the same defect as a fallback that passes for a hit. _workspace_has_documents can never break a query: vector count first, PageIndex trees second, any exception answers False. It decides which material to prefer, so it must not be the reason something fails. Verified live. The regression case now routes single_step, runs the full six-node trace, comes back grounded=true with X-RAG-Grounded: true, and answers from the manual: "No, an agent cannot sign its own work... he may propose and may never authorise." The control holds too — "hi" still routes no_retrieval and skips retrieval entirely, so the guard has not turned every greeting into a lookup. On this run the router chose single_step by itself and the backstop was not needed; the prompt did the work. Both layers stay, because the prompt is advice and the backstop is the guarantee. Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- Release: 1.24.0