NAOMS Devlog

Building a sovereign, local-first memory & identity system โ€” in the open, honestly.

NAOMS engineering update

Week 9

2,509 commits, including a migration of 142 governing rules. Everything the week touched is below, the largest threads first and every area at the end.

๐Ÿ“… May 11 โ€“ May 17 ยท Process
Commits
2,509
Policy rules migrated
142
Peers in a green call
3
Verification screenshots
181
Busiest day
2026-05-13
Process Dispatch free May 17, 2026ยท22 min readยทmeta

Commits per day โ€” Week 9

Four things changed this week

One per major area, and the week's shape was front-loaded: busiest on 13 May, tapering toward the weekend.

1

Every operator role now lives on one screen

Finding a role, taking it and handing it back happen in one place, replacing the per-feature screens they used to be scattered across.

2

The rules that govern NAOMS became readable data

142 rules moved from hand-written code into plain declarative phrases a single engine reads, and a comparator proved each rule still matched exactly what it had matched before.

3

A three-peer call carried audio, video and live transcription

Three participants, real media in both directions, speech turning into text as they spoke โ€” passing, with all three peers running on one machine.

4

Adding a contact became a ceremony both sides sign

Turning a stranger into a contact runs as a mutual exchange now, signed into both people's own permanent records rather than asserted by a server.

Finding a role should not require knowing where it lives

The feature: operator roles โ€” the jobs a person or a machine can take on inside NAOMS โ€” and the act of finding one, taking it, and handing it back.

Before: each kind of role was administered from the screen belonging to the feature that minted it, so holding two roles meant knowing two places and learning two layouts. Now: one surface answers all three questions, with the same actions available on every row. (One screen to find and enrol in every role)

A directory is worth more than the sum of the things it lists. Scattering role management across the features that use it is locally sensible every single time โ€” the role belongs to the feature, so the screen does too. What it costs is the one question a person actually has, which is not "how do I join this role" but "what roles are there at all".

What a role is here, and why that decides the screen

A role in NAOMS is not a row in a permissions table. It is a signed claim, recorded on a chain, saying that this identity holds this role in this scope, granted by that authority, until revoked.

That difference is not a technicality. It decides the shape of the whole surface.

Because a role is a credential rather than a flag, the set of roles you could take is a live question rather than a fixed menu.

It is whatever is being offered in the scopes visible to you, so finding them means asking the graph of who belongs to what, rather than reading a list somebody typed out in advance.

Revocation follows the same logic. You do not remove a role by deleting the record of it; you append a revocation that supersedes the grant, and the history stays intact.

So the surface has to render a roster with a timeline behind it, rather than a list you quietly edit in place.

Once both of those are true, the old arrangement stops making sense. Every bolted-on per-feature screen was re-implementing discovery against its own corner of the same graph, and not one of them could show a person the whole picture of what they held.

Three panes for three questions

Available lists the roles you could take, found by asking which are being offered in the scopes visible to you. My Roles lists what you currently hold, with the history of where each grant came from. Roster lists, for roles you steward, who else holds them.

Keeping discovery and enrolment on one surface is about the loop a person actually runs, not about saving a screen. That loop โ€” see what is available, take one, later check what you hold, hand something back โ€” crosses the discovery boundary on every single pass.

Splitting them across screens forces a context switch in the middle of one mental task, and duplicates the underlying read: what you could take and what you do hold are the same query with different filters.

The bulk hand-back flow that landed on 14 May is the clearest payoff. Select several rows, revoke once, give a reason, confirm, and watch the grants leave the graph โ€” one gesture, because the roster, the action and the record of it share a surface.

Visibility and authority are two axes, not one

A late change on 17 May turned the surface green, and it is the most transferable decision in the whole feature. The roster is now visible to every member; only the actions on it require steward authority.

Before that, the entire roster pane sat behind a steward check, so an ordinary member could not see who else held a role they themselves held.

That is the wrong cut. Seeing who is in your group and being allowed to change who is in your group are different permissions, and collapsing them denies people information they have every right to.

Most access bugs turn out to be one of those two axes leaking into the other. Drawing the line explicitly, on the surface itself, is most of the design work.

A surface you can point at

Following a link straight into a specific role now lands you on that role inside the roster, rather than at the top of a list you then have to search through. A configuration address with a fragment routes directly to the right pane.

That matters more than it sounds. When something happens to a role โ€” its signing keys change, say โ€” the notification about it can carry a link dropping a steward exactly onto the roster for that role.

A deep link that silently lands you on the wrong pane is worse than no deep link at all, so the malformed-address fallback was captured and checked alongside the working one.

What it cost to prove

181 screenshots dated 15 May stand behind this, walking the surface state by state rather than sampling it: browser loaded, tab activated, roster rendered, two rows checked, the bulk bar visible, the modal open, a reason typed, submit done, grants gone from the graph.

Roughly half of those captures are of the feature failing on purpose. A roster that is empty because there is genuinely nothing to show. The bulk-action bar correctly absent when nothing is selected. A malformed link landing on the configuration fallback instead of guessing. (181 screenshots before we called it done)

Those are the frames that carry the argument. The passing frames prove the happy path is wired; the failing frames prove the edges are.

A surface that knows the difference between "nothing here" and "broken" degrades the way it was designed to, rather than however the runtime felt that day.

One capture from that day recorded a test asserting a slide-out detail panel the design never had.

The honest fix was to recognise that the row already is the detail โ€” it carries the member, the grant and the history inline โ€” and correct the test, rather than grow a panel nobody wanted in order to satisfy it.

What is still open. The work is close to finished rather than finished. The roster is in and verified, and its last hard defect โ€” the steward-authorisation edge above โ€” closed on 17 May. The final review had not completed as the week ended, and this report says nearly done rather than rounding it up.

Rules you can read beat rules you have to run

The feature: the rules that govern how NAOMS behaves โ€” what is allowed, what must happen first, what is refused outright.

Before: each rule was its own small piece of hand-written code, so the only way to know what a rule said was to read the program enforcing it. Now: 142 of them are plain declarative phrases that a single shared engine reads, and the hand-written machinery that used to interpret them is gone. (When the rules became readable policy)

A rule expressed as code can only be audited by someone who can read code. That is the whole argument. A governing rule has an audience wider than the people who implement it, and every step that widens the audience without weakening the rule is worth taking.

The rule is the gate, not a description of one

The rules here are not advice an actor is trusted to remember. Before the AI facilitator changes anything, it announces what it intends to do; that announcement is what unlocks the tools that change things. Skip it, and its hands stay locked. (The agent can't skip the rules)

There are two beats, and the distinction is worth holding. Announcing the intent up front is cooperative โ€” it records that the consultation happened. The judgement of whether the specific action is allowed happens later, at the moment the action actually lands, and that beat is enforced no matter what.

Each standing rule is defined as its gate rather than described alongside one, and that is what makes the arrangement hold over years.

In most systems the written rules and the enforced behaviour drift apart until the document is archaeology. Here a rule that stopped being enforced would stop being a rule, because the gate is the rule's body rather than a second copy of it that can rot.

Some rules cannot wait for the next natural checkpoint. When the engine returns what applies to an intended action, a few carry an instruction to do something before continuing, and following those is strict rather than advisory.

The rules are also protected from the actor they govern. A fast, capable facilitator that finds them slowing it down might reasonably wonder about editing the rules.

Changes to the governing set must themselves pass governance. A rule can be changed โ€” just never quietly, and never unilaterally, by the thing trying to escape it.

Why deleting the old machinery is the evidence

A declarative rule set replaces the evaluators or it has not replaced anything. Adding a readable layer on top of the code it was meant to supersede leaves two places for the truth to live, and the two will disagree inside a month.

The migration landed in two halves on the same day. One half was the engine side: a single evaluator handling every declarative rule, plus a comparator diffing what the new phrases match against what the old code matched.

The other half was the migration itself, rule by rule, with the old machinery removed as it went.

The comparator is the part that makes this an engineering claim rather than a hope. It lets you prove the move was behaviour-preserving one rule at a time, instead of shipping 142 rewrites and trusting that nothing shifted meaning on the way across.

One rule turned out to be two

One rule split into two on the way across, taking 142 rules to 143. "Do not capitulate under pressure" and "do not endlessly ask for confirmation" had been living in one tangled evaluator.

They are genuinely different failures. One is giving up when pushed; the other is refusing to act on a path that was never ambiguous. In code form the seam between them was invisible. In declarative form it was obvious, and each can now be matched, reported and argued about on its own.

That is the quiet argument for the whole exercise. The declarative form also changes who can object to a rule: a phrase can be read aloud in a discussion and disagreed with by somebody who will never open the codebase, which is the point of writing rules down at all.

There is a cost, and it deserves naming. Declarative rules are only as expressive as the vocabulary they are written in, so a rule that vocabulary cannot say has to either stretch it or stay as code.

That pressure is real, and it points the right way โ€” toward a small, stated set of things a rule is allowed to talk about.

The unified operator roles surface, captured 2026-05-15 โ€” one of 181 verification screenshots taken before the feature was called done. This is a real interface capture, not a generated chart.

What this means, in plain terms

Five general lessons this week's work paid for, each learned by getting something wrong somewhere first.

Seeing who is in the room is not permission to change who is in it

The operator roster sat entirely behind a steward check, so an ordinary member could not see who else held a role they themselves held. It now renders for everyone while the revoke actions stay steward-gated. Visibility and authority feel like one question and are two; most access bugs are one leaking into the other.

A feature you have only watched succeed is one you do not understand

Half of the 181 captures behind the roles surface are of it failing on purpose โ€” an empty roster, an absent bulk bar, a malformed link hitting its fallback. The passing frames prove the path; the failing frames prove the edges. Evidence that contains no failures is a demonstration, not a test.

Deleting the old path is what proves the new one

142 rules became declarative phrases, and the hand-written machinery that used to interpret them was removed in the same landing. Had both survived, there would now be two answers to every question about what a rule says. A migration that leaves the old mechanism running is an addition, not a migration.

Descend the ladder you already have

An overnight failure that spanned several processes was root-caused by running the existing tests from the lowest level upward until one went red, then reading what it printed โ€” two real bugs surfaced in output already on the screen. Do not build a new instrument in the dark; pick up the one already on the bench.

Name the part of the path your test did not cross

A three-peer call with audio, video and live transcription passed this week with all three peers on one machine. That is a real milestone and it is not the feature, which is a call between machines. State the boundary your evidence stopped at in the same sentence as the result, or the result will be read as covering it.

How much healthier is it than a week ago?

CommitsLandings on the shared codebase's main line of history inside the window, counted once each along the first-parent path
2,509
Policy rules migratedRules converted from hand-written evaluators to declarative phrases
142
Peers in a green callParticipants in the call that carried audio, video and live transcription, all three on one machine
3
Verification screenshotsCaptures dated 2026-05-15 taken across the operator-roles surface before it was called done
181

The week was front-loaded: 13 May was its busiest day, and volume tapered toward the weekend. No net line-count figure appears, because a week of this shape is dominated by generated material and fixtures, and a net figure would describe those rather than the work.

Three honest notes

  1. The operator-roles screen is nearly done, not done. It is in and verified against 181 screenshots, with its last hard defect closed on 17 May. Its final review had not completed when the week ended.

  2. Voice and video are partial. The three-peer call that went green ran with every peer on the same machine. The cross-machine, live-frame path was not closed this week, and the same-day verification says so plainly.

  3. The daemon conversion is mid-flight. Several waves of the blocking-to- concurrent conversion landed this week; the remaining waves have not.

In one line

every operator role moved onto one verified screen, 142 governing rules became readable data with each one proved to still mean what it meant before, the daemon's ceiling became your hardware instead of one thread, and a three-peer call went green on a single machine.

What changed, area by area

Every area that moved this week. Per-area file counts are not available for this window, so the areas are listed by what landed rather than ranked by traffic.

Operator roles.

One screen now finds, takes and hands back every role, replacing the scattered per-feature screens.

Deep-link routing into a specific role landed 15 May with its malformed-address fallback captured too, the bulk hand-back round trip landed 14 May, and the last hard defect โ€” an edge in steward authorisation that hid the whole roster from ordinary members โ€” closed on 17 May.

A stray test asserting a slide-out detail panel the design never had was corrected rather than satisfied, on the reasoning that the roster row already carries the member, the grant and the history inline.

Policies and rules.

142 governing rules became plain declarative phrases instead of hand-written code.

The legacy machinery that interpreted them was deleted in the same work, a comparator proved the move behaviour-preserving rule by rule, and one conflated rule split into two, taking the count to 143.

The split separated giving up under pressure from refusing to act on an unambiguous path โ€” two different failures that had been sharing one evaluator, and whose seam only became visible once the rules stopped being programs.

The permission gate.

The AI facilitator must announce an intended action before the tools that change anything will unlock, and the action itself is judged again when it lands.

Rules that cannot wait carry an act-before-continuing instruction, and any change to the governing rule set must itself pass governance โ€” so a rule can be changed, just never quietly and never unilaterally by the actor it constrains. ( The agent can't skip the rules )

Voice and video.

A three-peer call carrying audio, video and live transcription went green with all peers on one machine, and the race behind an earlier failure was root-caused rather than papered over: when a call lives on a channel and a third peer joins, the peers could briefly disagree about who may write to the shared call state.

Seeding the joined-member record at the moment the channel is created closes that window. Three is where a call stops being a pipe and becomes a group โ€” two peers never meet that race, and three do, which is why three is the honest threshold for saying this works.

( Three peers, audio + video + transcription โ€” green, but not done )

Contacts.

Turning a stranger into a contact is now a mutual ceremony rather than a one-sided tap.

One side generates an invitation, the other initiates, the first responds, and the two exchange relationship cards each authored for the other โ€” then sign an agreement anchored into both of their own permanent records, after which scoped sharing opens and nothing wider.

A unilateral declaration of trust produces nothing here by design, because a relationship only one side agreed to is a claim about somebody else rather than a relationship. Ambient nearby-discovery was removed deliberately: you should enter someone's graph because one of you offered, not because you happened to share a network.

( The ceremony that turns a stranger into a contact )

What a relationship card is.

The card each side hands over is not a profile.

A profile is one-sided and says the same thing to everybody; a card is the face of yourself you chose to show this particular relationship, so a close friend and an acquaintance can hold different ones without you keeping a single global profile that leaks the same data to all of them.

The card also carries the keys that let each side verify the other's later updates.

The parts that would keep a card alive over time โ€” recording which card a given peer was shown, and pushing a changed field out to everyone holding it โ€” are designed and partly built rather than shipped, so a card can still go stale.

Verifiable honesty.

A test-mode shortcut that silently installed an automatic yes in the approval path was removed from the shipped code entirely, so the automatic answer now has to be handed in explicitly by the tests that want it.

The behaviour is identical from outside; what changed is that a consequential decision is visible at the place it is made. The standard applied was not "is this dangerous" โ€” it plainly was not โ€” but "can you see it happening where it happens". ( You don't have to take our word for it )

Two kinds of lie, two defences.

A system can rewrite what happened, or it can state a guess in the same flat voice it uses for a fact.

The first is answered by the append-only signed record, where an edit breaks the chain visibly for anyone holding it โ€” including a stranger who trusts nobody here โ€” and a correction is appended rather than swapped in.

The second is answered by vocabulary: pieces of knowledge carry how they were come by, and the system describes an activation pattern rather than claiming to know an emotion it cannot see.

Delivery routing.

A device that received an entry was queuing a delivery addressed to itself, then failing loudly to reach itself.

Four separate passes build the list of who needs a copy โ€” the membership graph, the chain's owner, anyone named in the entry, and the founding member set โ€” and every one of them excluded the author while none excluded the reader.

A single guard at the point all four funnel through now returns early when the recipient resolves to the local identity, and a regression test drives the real queue from both directions, so it fails if the guard disappears and fails again if the guard ever starts swallowing entries meant for somebody else.

What the delivery queue declines to carry.

Some lanes hold the machinery of a single person's identity โ€” which devices are approved, which keys are trusted, which delegations exist โ€” and a copy of that landing outside the circle is the thing the boundary exists to stop.

The refusal needs three conditions at once: a private lane, a private kind of entry, and a recipient the identity has not already vouched for.

If the check itself cannot answer, it fails closed for the private kinds and open for everything else, on the reasoning that a transient blip is no reason to relax a privacy boundary and every reason not to strand ordinary traffic.

How a change reaches the right devices.

What is written for a recipient is a row addressed to that one device, holding the record, the lane, the entry, an urgency and an expiry thirty days out.

A flush a fraction of a second later delivers those rows directly, device to device; rows for a machine that is not reachable wait for it to come back, and rows that expire undelivered are swept.

A member whose role is to cache rather than participate is queued but deliberately left to the next batch, so the timing leaks less about who is actually in a conversation.

Asking to pull instead of being pushed.

A recipient can record, lane by lane, that it would rather fetch a lane on demand than have it streamed at it โ€” useful for a bulk content lane.

When that preference is set, no row is queued at all, so a device reconnecting later does not receive a backlog of the lane it opted out of.

Two lanes sit outside this entirely: the rules of a group and its metadata are pushed regardless, because members who cannot read the rules cannot check each other's authority to do anything.

Enumerate generously, then narrow once.

The four recipient passes overlap on purpose, because each returns the right answer at a moment when the others return nothing โ€” the membership index is still being built out of the very entry being delivered when a group is formed.

The price of that generosity is candidates arriving from several directions, and the discipline that makes it safe is doing all the narrowing at one point, where the context to narrow correctly is already in hand.

Benchmarks.

The performance suite now wraps every result in a signed record over a canonical form, so a claim that something got faster carries a signature rather than a recollection.

A three-peer call datapoint recorded 300 of 300 frames with a median frame time of six to ten milliseconds, alongside a warm-boot timing and a large-file throughput measurement. ( Benchmarking when the answer isn't a number )

Scoring what has no number.

Questions like "did it recall the right thing?" have no stopwatch answer, so the suite scores them with a second model as judge โ€” constrained to a one-word verdict so the harness can read it with plain code rather than needing a third model to interpret the second.

Exact string matching rejects a right answer phrased differently; a keyword check accepts "not Thursday" as a match for Thursday. Everything a counter can answer is still answered by a counter, and a judge's error rate is something to measure against hand-grading rather than assume away.

Calibrating a judge before trusting it.

A model grading a model is not free of error, and its mistakes are correlated โ€” it will misjudge a particular phrasing the same way every time.

So a sample is hand-graded and checked against the judge before the judge is trusted at scale, and the model and its settings are written into the stored record beside the verdict. The aim is not a perfect judge but a known error rate, so a pass figure carries an honest margin.

Debugging practice.

A cross-process failure is now root-caused by descending the tests that already exist until one goes red, rather than by building a fresh probe under pressure.

Two real bugs that night came out of output a running ceremony was already printing.

The instinct being corrected runs both ways: re-running the whole end-to-end test tells you that something broke and almost never where, while stopping to author a new test in the middle of a fire produces a reproduction rather than a diagnosis. ( Don't write an integration test โ€” use one )

The daemon.

The native bridge's conversion from blocking to concurrent continued through several waves, so the daemon is bounded by your machine's resources rather than by a single thread.

The conversion is not finished.

Model installation.

Installing a model from the marketplace now carries through to device assignment without a second step, and an invited member's devices bind end to end.

That binding landed at the close of the week.

Storage classification.

Every write is now routed through one classification before it reaches storage: a durable commitment that bears on governance is anchored, ephemeral high-volume state is not, and anything that fits neither is surfaced as ambiguous instead of being guessed at.

The third bucket is the load-bearing one โ€” a guess made at storage time is unrecoverable later, because nothing records that a guess occurred.

The terminal interface.

In progress, and not claimed as landed here.

โ† more in Process   home โœฆ   all โ†’