# Changelog

All notable changes to `@sym-bot/xmesh`. Every install learns: the grounded learning
subsystem ships in the free artifact too, as an opaque bundle from the closed core
(`@sym-bot/xmesh-core`) — capability has no tiers; only production licensing does.

## 0.11.0 — 2026-10-03 — the record is the spine, a doer's repository is read from outside it, and missions stay staffed under load

0.10.11 was never released: everything it was to carry is listed here, in 0.11.0.

**Upgrade from 0.10.x.** 0.10.x runs a doer's `CHECK: run` commands and its git reads unsandboxed (see "Fixed — security"
below). At its first start 0.11.0 migrates 0.10.x's money files onto the record and keeps the old files as `.v1`; see
"Migration".

**Changed — the record is the spine** (docs/DESIGN-record-spine.md v2)
- What a mission earned lived in many stores with no transaction between them: the registry's `eodAt`, `eodMatched`,
  `eodPayoutOwed`, `acceptedBy` and `handedTo`, the authority ledger, the end-of-day run files and their index, the
  0.10.11 journal, the roster's lesson digests and `svaf-profiles`. It now lives in ONE signed, append-only event log,
  `<state>/record/events.jsonl`, and everything else is a projection of it, behind the same read APIs:
  - an event is `{v: 2, seq, prev, at, issuer, kind, key, body, sig}`, signed with Ed25519 over its JCS form under the
    domain tag `xmesh-record-v2`, `prev` the hash of the previous line's bytes, the line its own canonical form, so a
    byte changed anywhere breaks verification at that event;
  - the key is unique: an append is insert-if-absent, returns the event already there when the bodies match, and reports
    a conflict when they differ. One payout and one penalty per (commission, role), one reversal per payout, one match
    per commission, one lesson per (commission, node): a double positive payment, a double penalty and a lesson taught
    twice cannot be recorded;
  - one writer, enforced by the operating system: the runtime holds an exclusive lock on the file for as long as it is
    open (O_EXLOCK on macOS, an exclusive open on Windows, flock(2) on Linux, the state lease alone where a host has no
    flock(1), said once), and closes it, synced, before it releases its state lease, so a new generation opens it only
    after the old one has let it go, and an old generation's late append is refused. On Linux the runtime opens the file
    itself (mode 0600) and runs `flock -xn 3` once on that descriptor: flock(2) locks the open file description the
    runtime shares, so the lock is the runtime's own, held while its descriptor is open and given back when it closes or
    the process ends (a SIGKILL too). There is no holder process, no polling and no file beside the record.
    `xmesh record repair` and `xmesh record migrate` take the state lease the runtime takes, so they refuse beside a
    live runtime;
  - appends are checked, numbered and folded at once, in the order the rules ran, and group-committed off the event
    loop: a line's `prev` and signature are made when it is written, in 20 ms slices with the loop's turn between them
    (a line costs 67-90 µs to make, six times its append, so a batch made in one piece undid the slicing of whatever
    appended it: 5,000 appends held the loop for 337 ms, now for 21 ms at most), and an event is acknowledged only
    after an fsync that includes it. A batch the disk refuses is cut back to the last acknowledged byte and written
    again (after a backoff, while the disk refuses; status says so), with every append since waiting behind it: no
    acknowledged event is ever dropped and no gap opens in `seq` or `prev`. Only a cut-back that fails too breaks the
    record. A blob (a completion's evidence) is written and fsynced, its directory fsynced once per batch off the loop,
    before any event that names it, and a closing runtime writes the blobs still pending;
  - damage is never silent: a tail that does not verify is moved to `events.quarantine-<at>`, never truncated, and the
    record refuses appends until an operator resolves it. `xmesh record verify` reads it (exit 1 when broken), and
    `xmesh record repair --restore <copy>` or `--accept-loss` resolves a break with the runtime stopped. The health
    endpoint says `record: appending | broken | refusing`; `/api/v1/_/record` has the detail;
  - **what acts follows what is on disk.** The fold an append makes at once is the rules' view, read only to check the
    next append, and every rule decides on it (a release, a verdict, a resume, a hand-off, the matcher's guards), so a
    decision made just after an append it depends on is never made against the past. The rows, authority, the end-of-day pages, a membrane, every listener and every effect outside the
    record (a delivery written or discarded, a spool dropped, a world torn down, a confirmation) follow an event only
    once an fsync includes it. **While the disk refuses a write, the record refuses new appends**, saying why, and the
    health endpoint says `record: refusing`; it takes them again once a write succeeds. A directory fsync that fails is
    an error (what waited on it is not durable) everywhere but Windows, which cannot sync a directory;
  - a process without the writer's lock reads the record read-only (`readRecord`): as far as its last complete,
    verified line, never writing (no quarantine, marker, checkpoint or lock). A line the runtime is still writing is
    left out, never read as a break. `xmesh record verify` is this reader, so it runs beside a running runtime;
  - a signed checkpoint every 10,000 events holds the head and the projections' state: a boot verifies signatures only
    past it, and verifies the prefix off the loop afterwards. Large bodies (a completion's outputs and evidence) are
    stored by digest under `record/blobs/`;
  - what the signature proves, stated honestly: the issuer key is the file the authority ledger always kept, and it
    protects against tampering by anyone without that key; a process running as the same user can read it. Each
    end-of-day pass emits the record's head hash in a CMB the operator node signs, so a rewritten history contradicts
    what peers hold. When sym 0.14.0 lands the issuer key becomes the operator node's key, through a rotation event.
- **The registry keeps descriptive fields only.** A commission's status, claim, completion, counterparties, match, owed
  payout and history are folded from the record onto the row in memory and never written to `commissions.json`. Its
  stops are on the record too: a worker's blocker, the supervisor's pause and a capability the runtime refused are each
  a `stop` event (blocked or paused, with its reason and who stopped it), and an operator's resume is a `resume` event,
  each keyed by the event it follows. A registry write lost after a stop loses nothing: the row is still stopped, with
  its reason, after a restart. Readers beside the runtime (a critic's owed reviews, the delivery ledger's root walk) read
  `commissions.view.json`, a projection written for them.
- **Authority is a projection of the record.** The fold, stated: events in (at, seq) order; a `reversal` cancels its
  payout and gives back that payout's share of the commission's budget; a `penalty` carries the negative outcome apart,
  so a critic who objected correctly after an end-of-day payout is credited instead of finding the budget spent; the
  doer's outcome takes the verdict's trust horizon whichever path appended it first. A dismissal after a payout is now a
  reversal and a penalty (it was a second, negative entry beside the payout).
- **End of day is a pass that appends what is missing**, and keeps what its tests pinned: a run line per booking kind
  (delivered, rca, abandoned, diagnostic) with everything the blotter, the breaks queue and the late breaks read; the
  bookings taken before the judgement read; the loop's turn between slices; a payout for a matched delivery with a known
  doer and for a completed→validated verdict, never for a ruling on a stop or a recovery; the human's queue of breaks,
  owed payouts (matched, recipient unknown, or refused, each with its reason) and late breaks. A person's ruling on an
  owed payout (`POST /api/v1/_/eod/owed/:key {settles: pay | forgo}`) settles it. A crash anywhere means the next pass
  appends only what is missing. A pass that matched nothing appends nothing, so it is no longer listed as a run.
- **The lifecycle folds in `seq` order**, the order the rules were checked in: an accept stamped before its offer by a
  clock that stepped back folds after it, and the row is its acceptor's. An event's `at` is when it happened, read by
  what measures time (authority's decay, the history's times). The migration appends a 0.10.x history in time order, so
  the fold is one linear pass that never rebuilds from empty: one build of each view (the rules' and the durable one,
  at the record's open) for 200, 800 and 1,600 migrated rows.
- **The record carries every commission's existence.** Its first offer holds what it asks, its room, its parent and its
  criteria: a commission whose registry row was lost (a write that never landed, a registry restored from before it) is
  listed again from the record. The end-of-day pass, the money and `settle` read the record's commissions, never only
  the registry file's rows.
- **A membrane is the replay of its lessons**, in `seq` order (a lesson stamped earlier by a clock that stepped back
  replays where it was recorded, as a replay from empty does). The private rule is applied purely, given (profile,
  categories, verified, time); each `lesson` event carries those inputs and the stamp they reached, and the roster's
  profile is a cache naming the seq it folds to. `applyOutcome` no longer writes `svaf-profiles/*.jsonl`; a node's history is its lessons.
- **Offers have identity** (design S5, the record's side). A request is its own first offer, to its room; a hand-off, a
  re-offer, the membrane fallback, a restart's claim hand-back, a resume and a restored worker each make a new offer
  that supersedes the open one, and a spawned worker is given its offer (`--offer`). An accept names the offer it
  answers (`accepts <K>@<O>`), and the runtime takes it only when that offer is the open one, nobody has accepted it,
  and the acceptor is one the offer is to; volunteering is an accept of the room's open offer, so concurrent volunteers
  never both take it. A refused spawn withdraws its offer, which re-opens the one it superseded. Every accept is
  answered, positive or naming the open offer, and **a worker does nothing with side effects until it holds a positive
  answer**: a queued copy of a request the runtime handed to a fallback is refused and steps aside. A worker that missed
  an answer asks for the open offer. **A completion names its offer** (`completes <K>@<O>`) and books only under its
  author's live accepted offer, the one no later offer superseded: a commission has at most one, so two valid
  completions cannot exist. The current holder's completion wins; a worker whose accept a release, a resume or a
  hand-off superseded is kept as a trace, with the reason. A WF-13 peer answer and the runtime's disk recovery are the
  exceptions, each recorded with its basis. A hand-off's addressee that releases it ends the hand-off (the offer is
  withdrawn, re-opening the one it superseded), and a released offer refuses its worker's accept again only until
  something re-opens it, so a withdrawn hand-off makes the room doer's queued copy valid again.
  - **The wire is one adapter, on the existing grammar until sym 0.14.0** (src/offer-wire.ts): the accept rides the
    `accepts` verb, its answer an `accept-ack` and the offer query an `offer-query` on directed CMBs the room operator
    signs. Design S5 carries them as an XMesh application extension (`xmesh-offer-v1`, `xmesh-accept-v1`,
    `xmesh-withdraw-v1`, `xmesh-accept-ack-v1`, `xmesh-offer-query-v1` in `metadata.application`), which needs sym
    0.14.0's v2.0 records; the adapter is re-implemented over them then, and nothing that calls it changes.
  - **A worker that steps aside releases what it accepted** (`releases <K>@<O>`): every accept it sent and never saw
    confirmed, when no answer came or the answer was a refusal. Its decision is a record fact (`release`). When the live
    link lost the accept and the store sweep took it, the sweep replays the worker's records in their order, so the
    release after it gives the row back: it is offered to its room again, and the re-offer rescues it like any open row.
    A worker's accept of an offer it released is never taken, whichever reached the runtime first, so a row is never left
    held by a worker that is not running it. A worker that dies holding an accept is still released by the reaper. The
    sweep's severed-wire recovery (a worker's accept, progress and completion all found in its store) is unchanged.
  - **Two rules are transitional, until sym 0.14.0:**
    - an accept that names no offer (a worker older than 0.11.0, or one that heard only the request) answers the request
      itself, which is its own first offer: it is taken while that offer is open, and refused, with the open offer named,
      once a hand-off, a release, a resume or a re-offer superseded it. What tightens it: the adapter's move to v2.0
      records, where `xmesh-accept-v1` carries the offer it answers as a required field, so an accept without one is
      refused;
    - membership of a room is what the runtime observes: an offer to a room is taken from an author heard in that room
      (or where the commission's work is done), staffed into it by this runtime, or registered in its mesh on this host,
      and from no one else (server/room-membership.ts). The control plane passes the room it heard a record in, so a
      peer host's worker heard in the room is taken live, not only by the store sweep. What tightens it: sym 0.14.0's proven room membership, after
      which an offer to a room is taken only from a node whose membership of that room is proven, and an offer to a node
      only from that node's proven identity.

- **And, on the record's side:**
  - a person's verdict changes nothing else (the lifecycle file, the author's store weight, the verdict CMB, the row,
    its listeners), and the endpoint answers nothing, before the verdict event is on disk. The wait is bounded: past it,
    the verdict and everything appended behind it are taken back, never written, and the answer is 503 "the record
    could not write: nothing was done", which is then true (server/verdict.ts);
  - a stop is keyed by what carried it (the blocker CMB), so a blocker replayed after a resume is the same stop;
  - one rescue per commission across restarts rests on the record's re-offer offers, not on a debounced registry note;
  - an owed payout whose recipient is unknown is not payable: the queue says so, and only `forgo` settles it; a break a
    person validated, whose payout the record refused, stays in the queue;
  - the issuer key is never minted over a record or a ledger it signed (a lost key is restored, never replaced): the
    record names the key that signs it when it is made (`record/issuer.json`, the key's fingerprint), no key is minted
    for an issuer while its record (any file: events, mark, checkpoint, quarantine, break marker) or its own v1 ledger
    exists, and a record signed by another key is refused at open, saying which key it needs;
  - `xmesh record verify` reports a record it cannot read (exit 1), and only a missing file reads as empty;
  - the registry and its view are stringified once per change, not the whole registry twice.

**Fixed — a mission's doer is staffed for its commission.** Under load a new mission's doer could miss the one
delivery of its request: it sat idle until the idle reaper took it, and the re-offer sweep, which counted the mission's
started workers as staffing, never offered the request again, so the mission waited for good (the capacity daemon test,
2 Oct 2026). Now:
- the mission's commission is minted first, and its doer is staffed for it, as a rescue, a resume and a re-offer are:
  given its goal and its offer (`--goal K --offer O`), it reads the request from the store and accepts it, whatever the
  mesh delivered. One staffing path. A retry of a deferred assembly takes up the commission its earlier attempt minted,
  and a mission that will not be retried closes it;
- the re-offer sweep counts as staffing only a worker that is alive (a critic never: it does not take the work);
- a worker staffed for one job leaves, freeing its mind slot, when its accept is not taken or the job does not reach it
  within `XMESH_GOAL_WAIT_MS` (90 s).

**Fixed — the runtime's answer to a worker never rides one live delivery.** Under load a worker's link to its room's
operator came up late: its accept went nowhere live, the store sweep took it unanswered ("a store sweep's replay has
none: its author is not waiting"), and the worker gave up ("answered neither the accept nor an offer query") with the
row held in its name (mission-staffing.test.ts at load 35 on feat/0.11.0, 2 Oct 2026). Now every route that decides an
accept or an offer query answers it (the control plane, the lifecycle inbox, the store sweep), and every answer goes
live and to the worker's answer file (`<state>/worker-answers/<worker>.jsonl`, src/answer-file.ts), which the worker
reads every second. A same-host, same-user channel; a peer host's worker has the live route only. A worker reads only
answers written after it started.

**Migration.** At its first start (or `xmesh record migrate`, the runtime stopped) 0.11.0 moves 0.10.x's money files onto
the record, every v1 fact as exactly one event, key-idempotent so a migration a kill stopped converges: each v1 ledger
entry classified by (ge, subject, p, y) (the first positive for a commission and role becomes `payout`, the first
negative `penalty`, later ones and entries with no verdict key or about their own issuer `legacy`, and another issuer's
entries `legacy` with no voice, since 0.11.0 admits no foreign issuer); each registry row's
offer, accept, completion with its evidence, objections, approvals and a verdict per ruling in its history (only
completed→validated pays); a `match` for every row with `eodAt`, from its run line; leftover journals and `eodPayoutOwed`
rows as the attempts they stood for; `handedTo` as a hand-off offer; roster profiles, and each transition a membrane's
history logged (`svaf-profiles/`), as `profile-import`; every outcome 0.10.x taught a membrane (a verdict on a completion,
a counterparty's match of a delivery) as an imported lesson that applies nothing, so no migrated outcome is taught again
(0.10.10 kept no lesson ids, and 0.10.11's covered only the end of day); every stop in a history as a `stop` event, and
every stop the history shows lifted as a `resume`; the owner map as `alias` events. It takes any state a 0.10.x runtime
kept, 0.10.10's and the 0.10.11 candidates' alike. **History's money stays as 0.10.x left it:** every commission whose
money the v1 files say 0.10.x acted on (a match, a verdict, a ledger entry, an owed payout, a journal line) is marked
`settled`, decided from the v1 files alone, and implies no money from its match, its
verdicts or its objections: no reversal of a payout v1 kept beside a dismissal's penalty, no payout v1's ledger never
made, no critic scored again, and no lesson taught again. A payout the record would imply and v1 never made is in the
owed queue, and a person's `ruling` is the one way history's money changes; a completion made since the upgrade is new
work under the 0.11.0 rules. The report (`record/migrated-v1.json`) counts every
class and checks itself against the raw 0.10.x fold, what 0.10.x showed (admitted foreign issuers included): every
(agent, lane)'s authority and every row's lifecycle fields, against what the record projects. Each difference 0.11.0
makes on purpose is listed under `check.expected` with why: an admitted foreign issuer's voice
(`foreign-issuer-voice`), a recovery the runtime closed from disk, which 0.10.x credited to its acceptor and 0.11.0
credits to nobody (`disk-recovery-credited-to-nobody`), and on a restore what the record gained since the upgrade
(`since-the-upgrade`). `xmesh record migrate` exits 1 only on a difference nothing explains, so a correct restore
merge exits 0. **Admission is gone in 0.11.0:** 0.10.x let an operator admit a foreign issuer
(`admitted-issuers.json`), whose entries counted at a budgeted weight; 0.11.0 admits none, so **a subject vouched for
by an admitted issuer loses that authority at the upgrade**. The report lists each (agent, lane) it changes. The old files are renamed `.v1` and kept. **A migration that fails never loses the v1
history:** the registry is copied to `.v1` before anything is written; the runtime refuses to start, saying to run
`xmesh record migrate`, and nothing projects or saves over a registry that still holds 0.10.x rows. A registry that still
holds 0.10.x rows is taken in whenever one is found, on a state directory migrated before too (one moved or restored onto
it): it merges into what the runtime had. The runtime's current rows win for rows in both, the rows made since the
upgrade are kept, and what a worker reported (summary, artifact, checks, usage) comes from the record's evidence. Every event is planned from the v1
files and appended in time order, and no key or time is the run's clock, so a migration run again, or killed at any
point (an append, or after its flush and before its renames), converges on the same events. `--dry-run` migrates onto a
scratch record and writes nothing that lasts.

**Removed** (design §3): the end-of-day journal and its take-up; debts, `validOwed`, `mayBeOwed` and the debtors; remainders
and `eod/index.json`; `paidAlready` and `attestedVerdictKeys`; accept epochs, `@?<ms>`, `staleJob` timing, the
five-minute and fast-clock accept rules, `noteBlindAccept`, the 16/64 accept bounds and the 90 s hand-off hold;
`importLegacyOnce`; the roster's coalesced write of lesson digests; `exportLedger` and `importForeignLedger` (unused, and
over the record they would leak every kind), with the foreign-issuer admission they fed (`admitted-issuers.ts`).

**Fixed — one name, one worker**
- Two review (or assessment) missions filed together no longer fail one of them with "worker '…-critic' is already
  running". Both read the recipe room as having no critic, and the second start of the critic's name was refused. A
  recipe room is now staffed by one mission at a time, and a name another start already holds (a boot's restore of the
  roster) counts as the room's worker, not as a failure. A critic that cannot start for another reason is logged and
  the mission runs without it, as a crewed mission's does; a doer that cannot start still fails the mission.
- Two starts of one worker name at once could both spawn: the name was checked, the room's operator started, and only
  then was the worker registered. The second record replaced the first, and the first process ran on with no record.
  The name is now held from the check to the registration.
- A worker's exit removes only its own record. Stopping a worker and starting its name again (a change of mind) left
  the old process's exit to delete the new worker's record: a live worker nothing tracked or reaped, in a room that
  read as unstaffed.

**Fixed — a fallback doer's steps are its own**
- A worker's progress line is no longer refused as "a copy of another author's record" because its lineage parent is
  a key another worker also wrote. Two workers on one commission take the same steps ("reading canon-context.md"), and
  identical records share a key, so the parent's key could be both authors'. core-team logged 197 such refusals, every
  one a step by the commission's own doer (a re-offer or resume doer, or the room's doer after one) citing its own
  previous step. A parent the author has a verified record of is now its own. A refused copy is noted as its author's
  record, so the author's next steps are not refused in turn, and the refusal is logged once, not on every sweep. The
  store sweep, which does not append a line the trace already holds, still notes that line as its author's. A copy
  of another worker's record that its author never wrote is still refused (codex-win m154 (b)).

**Fixed — the embedding cache stops re-embedding what it cannot hold**
- A recall corpus larger than the embedding cache no longer keeps the encoder re-embedding it. Recall reads every
  cognition text of the categories it is asked about, on every query, and queues what the cache misses; the background
  embedder kept each miss by evicting the vector read longest ago, the one the next query reads first. core-team's
  corpus held 31,780 distinct texts against a cap of 20,000, so every query queued the same 11,780 texts again, and
  every write rewrote the whole 42 MB file. A full cache now gives up an idle vector first (one nothing has read for
  four times the cache's usual interval between reads, both counted in lookups, never in time), else a random one, and
  a text recall missed is embedded only into room the cache has: with none, it stays unencoded, without an inference,
  and recall reports it as missing (its partial line now says the cap, not the encode budget, left it so). Queries,
  recall's own bounded encode and newly admitted cognition still go in, and stay until something reads them: a vector
  put in is never the one a full cache gives up until it has been read, or until a sixteenth of the cap more have been
  put in after it. With the random choice over every entry, the vector a put had just made was as likely to go as any
  other, so a burst of 50 at a cap of 5,000 gave up one of its own in 21.8% of bursts, an inference lost before anything
  read it. Measured on a synthetic 31,780-text corpus, encoder stubbed, the background embedder drained between
  recalls, over three recalls: before (cap 20,000, LRU), hit rate 0 / 62.9 / 62.9%, 31,781 / 11,782 / 11,782 inferences
  and 41.9 MB written on every recall; now at the same cap, 0 / 62.9 / 62.9%, 20,000 / 0 / 0 inferences, and 41.8 MB
  written once, then nothing; now at the default cap of 60,000, 0 / 100 / 100%, 31,781 / 0 / 0 inferences, 66.4 MB
  written once. With recall's encode budget at its default of 64, a recall after the first encodes its 64 and appends
  them (134 KB), where the old cache re-embedded 11,782 and rewrote 41.9 MB. (Recall's own hit rate held at 62.9%
  before only because its scan never inserts; the cost was the re-embedding and the writes.) Where every miss is kept
  instead (a caller embedding each text of a cycle in turn, not recall), the hit rate at 1.5 times the cap is 39.6%,
  against 41.7% for a random choice that may take a vector just put in, and 0 for LRU.
- The cap is 60,000 by default, so core-team's whole corpus is held. Vectors are held as Float32Array, as the encoder
  makes them, not number[]: 1,984 bytes an entry instead of 3,428 (20,000 entries, keys and bookkeeping included), so
  core-team's 31,780 texts take 59.5 MB where 20,000 took 65.4 MB, and a full 60,000 takes 107 MB.
  `XMESH_EMBED_CACHE_MAX` sets another cap.
- The cache file is a log: a new vector is appended as one line, and a batch of 100 new texts writes 208,900 bytes where
  each write rewrote the whole file (41,860,001 bytes at 20,000 entries). It is compacted, rewritten from the cache in
  pieces with tmp + rename, when it holds more than twice the cache's entries, and at a load that finds it so. A line
  cut short by a crash mid-append is skipped, and the file is compacted before anything is appended to it. The file is
  now `embed-cache.v2.tsv` beside the old `embed-cache.json`: the 0.10.x file loads as before, with no cold start, is
  written to the new file right after, and is removed once that has landed. Going back to 0.10.x starts its cache
  cold, and never finds a file it would misread.
- The scheduler telemetry (`/api/v1/_/scheduler`) reports the cache's entries, cap and file lines, and the embeddings a
  full cache did not keep.

**Added — read-only git for a review's doer**
- A review mission's doer, and a review panel's lens, when it is a claude mind, can read git. Its run gets one MCP
  tool, `git` on the `xmesh-git` server, which runs `git show`, `git log`, `git diff` and `git ls-tree` and nothing
  else, on the repository's commits: it never reads the work tree or the index, so `git diff` compares commits (one
  with its parent, or two) and a work-tree file is read with Read. The review brief tells the doer to take every fact
  it cites from a commit from that tool (and to mark such a fact INFERRED when the tool is not among its tools, as for
  a codex doer); a lens is told a commit-cited ground is DERIVED only from the tool's output. No other run gets the
  tool. Before, a reviewer could not check the commit it was reviewing: git runs through Bash, which stays behind
  approval. (Planned for 0.10.12, which was not released; it ships in 0.11.0 on the git boundary below.)
- It reads a repository the way the runtime does (the security section below): the runtime finds the repository and
  its refs itself, git runs in a private repository of the runtime's over the doer's objects only, every config file is
  switched off, and the operating system's sandbox bounds what git can read, write and start. On a host with no
  enforcing sandbox it answers "not stated" and runs no git.
- It runs only in the directories the run already has (its workspace, the team root, the mission's project, a widened
  run's approved directories) and in the git repository of the file under review (for a lens, of the proof artifact
  and its evidence), compared by real path: a symlink that leads out of them is refused.
- Its arguments come from a doer, so each is decided before git starts, and a refusal names the argument and why:
  - an option is one the tool lists for that command (an allow-list, not a list of what to refuse), and a valued
    option is given as `--name=value`, never with its value as the next argument; anything else that starts with `-`
    is refused, an option that writes a file, runs a program or points git elsewhere among them;
  - every other argument before `--` is a revision (a ref, `HEAD`, an object id, with `~` and `^` suffixes, `A..B`,
    `^A`, or `<rev>:<path>`), and each is resolved to an object id in the private repository before git sees it, so
    git is given ids, never a doer's words where it parses options;
  - paths come after `--`, relative to the repository's root, and none starts with `-` or leaves the repository;
  - at most 64 arguments of at most 1024 characters, no NUL byte, and `A...B` is not supported.
- git runs without a shell and without any inherited `GIT_*` variable. A call has 30 s in all (its refs, its
  revisions and its command share them), output past 1 MiB is cut with a note on how to narrow it, and git's own
  complaints past 64 KiB are cut while it goes on.


**Fixed: a kept agent can no longer lose its identity**
- **The sweep could archive a kept agent's identity.** It moved crew stores aside after 30 idle
  days, and it recognised a kept agent only by its name. A worker preserved under its crew name
  (for example `mission-ab12cd34ef56-doer-1`) keeps that name, so the sweep moved its directory,
  `identity.json` included, to the archive. The next restore then started a new identity under
  the same name, with an empty store and none of the agent's history.
- **The sweep now reads what the runtime kept from its rosters.** Deployed agents and standing
  workers are never candidates, and neither is any alias under them (`<name>-…`). If a roster
  exists but can't be read, nothing is archived. A directory whose identity lock is held by a
  running process is never moved.
- **Restoring or preserving a kept agent never starts it under a new identity.** When its
  identity isn't on this host, it is refused and named, and its roster entry stays.
  `GET /api/v1/_/agents` lists these refusals as `refused`.
- No agent on this host was affected.

**Fixed — security**
- The runtime no longer runs programs from a repository a doer can write. To record a review target's commit, the
  runtime ran `git status` and `git hash-object` in the target's repository; for a mission's outputs, `git status`;
  for an output's diff, `git diff`. Each ran as the runtime, with its environment, in a repository that is often one a
  doer made (a doer can write files under the team root, `.git/config` and `.gitattributes` included), and that
  repository's config decided what else ran: its fsmonitor hook, its clean and process filters, its external diff, a
  diff driver's command or a textconv ran inside the runtime, and `git status` rewrote its `.git/index`. What the
  runtime now guarantees when it reads such a repository (a review target's, a deliverable's, the checkout `/version`
  reports):
  - It finds the repository and the commit itself. It reads `.git`, `HEAD`, the branch and `packed-refs` as text and
    never asks git where they are. A `.git` file is followed only to a directory inside the work tree, or to a
    submodule's directory in its superproject. A `.git` file that points at a repository elsewhere, a symlinked objects
    directory, a linked worktree whose repository is elsewhere, a reftable repository, a `HEAD` it cannot read and a
    chain of more than five symbolic refs are all "not stated": before, a `.git` file was followed anywhere, and that
    repository's committed bytes could come back as a deliverable's diff.
  - git never runs in the doer's git directory. Each read makes a private, empty repository of the runtime's own and
    reaches the doer's objects from it, so the doer's config, attributes, hooks, refs and replace refs never reach git,
    and git writes nothing into the doer's repository. Every setting git gets is on its command line; the system, user
    and XDG config files are switched off; the settings that name a program are still fixed, as a second layer.
  - The operating system bounds that git. On macOS it runs under `sandbox-exec`; on Linux under Landlock, through a
    small launcher of the runtime's own, which the container image builds. git may read only the doer's objects, its
    own installation and libraries, and the runtime's private directory for that read; it may write only that
    directory; and it may start no program but its own binary and helpers (on Linux the loader too): nothing in the
    doer's objects and nothing in the runtime's private directory is ever run. This is what stops the one thing git's
    options cannot: a doer's alternates file naming another repository, which git follows. Before relying on the
    sandbox, the runtime checks that it works: a test run must fail to read a file outside the allowed set, must fail
    to start `/bin/sh`, and must fail to start a program copied into the private directory or into an objects
    directory, and on Linux the launcher must refuse a rule path through a symbolic link and a directory other than the
    one the runtime checked. That check runs again whenever git, the launcher (or `sandbox-exec`), the shell or the
    profile changes; on a kernel whose Landlock is too old it is not run again at all (the kernel does not change).
    Where the sandbox does not work (Windows, a Linux kernel older than 6.2, a Linux install without the launcher), the
    runtime does not read a doer's repository with git at all: the git facts say "not stated — this host has no
    enforcing sandbox for reading a doer's repository", and the deliverable is still listed.
  - On Linux each sandbox rule is made on the directory the runtime checked, not on whatever its path names a moment
    later. A doer can change its repository while the runtime works, and the first build of this sandbox used the
    kernel's own sample helper, which opened each rule path following symbolic links: a doer that swapped the objects
    directory, or `.git`, for a link at the right moment had the rule granted on the link's target, and the runtime's
    git then read another directory's objects (found by an independent review, and reproduced in a container). The
    launcher now opens each rule path without resolving any symbolic link in it, and grants it only if it is the very
    directory the runtime recorded when it checked the path, so a link, or another directory renamed into its place, is
    refused before git starts. The runtime reads a deliverable's bytes and the repository's `HEAD`, refs and `.git` file
    the same way: through the path it checked, and only if the file it opened is the file at that path, and it walks
    a repository's refs that way too: a refs directory that is a link, or that changes while it is read, is refused
    rather than followed, and the walk is bounded in entries and in time. It needs Landlock ABI 3 (Linux 6.2), the first
    that stops a sandboxed program from truncating files outside its directory; on older kernels, Debian 12's 6.1
    among them, the host counts as having no enforcing sandbox. The sample helper is no longer shipped.
  - Each read gets its own private directory inside the runtime's state directory, which only this user can open, and
    it is removed when the read ends. Nothing is shared through the system temp directory any more, and when that
    directory cannot be made the git facts say so instead of the deliverable going missing.
  - Whether a file matches its commit is a byte fact: a file is clean only when its bytes are the commit's blob. A file
    whose line endings alone differ is reported as exactly that, never as clean (before, the runtime imitated git's
    line-ending rules, and a doer could make a changed file read clean). When the bytes differ and a `.gitattributes`
    of the commit's on that file's path names a filter, `ident` or a working-tree encoding (Git LFS is one), the git
    facts are "not stated — a filter the runtime does not run may apply" and no diff is made. A diff is
    `git diff --no-index` over two copies in the private directory.
  - Answers are honest about what they do not know: git failing or timing out, a repository the runtime does not read,
    no private directory, no sandbox, and the runtime's git not ready yet are each "not stated", never "clean", and the
    brief quotes nothing the repository chose. A git that runs past its time limit is stopped together with anything
    it started; it also runs under a CPU-time limit, and on Linux a memory limit. A git whose answer is larger than the
    runtime reads says so, never "no answer in time", and a flood of git's own complaints (a doer's alternates file
    can make git write hundreds of kilobytes of them on every start) is cut at 64 KiB while git goes on to its answer.
  - The container image's package is owned by root, with no group or other write, so the runtime's user, and a doer
    running as it, cannot rewrite the launcher or the module that decides what git and the checks may read. The one
    directory left writable is the embedding model's download cache.
  - The container image now carries git. Debian's git needs `zlib1g` and `perl-base`, which the image removes, so the
    image builds git 2.56.0 from source, with zlib 1.3.2 built from source and linked in statically, and without perl,
    Tcl/Tk, gettext, curl, expat, python or Rust. Both sources are pinned by version and sha256. Only the git binary
    goes into the image, and `zlib1g` and `perl-base` stay out.
  - What the sandboxes do not cover, stated: a hardlinked object (a path-based boundary cannot see a hardlink, and
    refusing an object with more than one link would break ordinary local clones, which hardlink by default — what a
    doer can hardlink into its workspace it can already read through its own CLI's sandbox, so that sandbox is the
    boundary for its reads, and the runtime's adds nothing there); on macOS, a directory a doer renames into the place
    of the one the runtime checked, which `sandbox-exec` then reads, because it checks each read against the path as it
    is at that moment (a directory the doer can rename is one it can already write, so this too is inside its own
    reach; on Linux the launcher refuses it); on Linux kernels before 6.12 (Landlock ABI 6), signals: a check can send
    a signal to another process of the runtime's user, the runtime included, which macOS's profile does not allow (on
    6.12 and later the launcher scopes signals and abstract unix sockets to the sandbox, and the check probe proves a
    check cannot signal a process outside it, as it does on macOS); on macOS, a check whose program the sandbox lets
    the runtime see is runnable but then will not start (the shell's own start of it failing after the sandbox said it
    was in force) reads as that check failing, not as the host's reason, so it fails rather than being "not verified"
    (the runtime checks the program can be run first, so this is a narrow case; on Linux the launcher reports it as the
    host's); a
    child a check starts that leaves its process group, closes every descriptor, clears its environment and changes
    out of its directory, which can outlive the reap, though it stays inside the sandbox the check ran under; and the
    cost of copying the workspace for each check, which is cloned on macOS and a real copy on Linux, capped at 1 GiB
    and 200,000 files. The image's git is built from source; Git 3.0 makes its Rust parts mandatory, so the next git
    upgrade in the image adds cargo to the build stage.
- A mission's `CHECK: run` commands run in the sandbox. They are commands a doer wrote, and they ran as the runtime,
  through the shell, in the doer's own directory, with the runtime's whole environment (its passcode and keys
  included). Now each command:
  - runs in a scratch copy of its workspace, made for that one check and deleted after, so the doer's own files are
    never changed by a check. The copy is made inside the check's own sandbox, so a directory the doer swaps for a link
    to elsewhere while it is copied cannot bring anything the check may not read into the copy;
  - may read only the directories it is graded in (for a worker's check: the worker's own directory, the deployment
    root and the mission's project; for a recovery from disk: the deliverable's directory) and the tools it needs
    (node and npm, the system's tools and libraries, and the system's configuration and shared data: `/etc` and
    `/usr/share`);
  - may execute programs directly only from its scratch copy (so `npm test` runs the copy's `node_modules/.bin`) and
    from the toolchain, never a program in the doer's own workspace. That holds for a direct exec only: the sandbox does
    not cover a file mapped by the system's loader or read by a shell, so `sh <file>`, or the loader given a file, can
    still run a file the check may read from the doer's workspace, as the doer itself already can;
  - may write only its scratch copy, and may open no network connection: on macOS the profile denies the network, and
    on Linux, because Landlock bounds only TCP, a seccomp filter denies `socket(2)` for every address family (UDP, unix
    and netlink included) and io_uring (which can make a socket without `socket(2)`), and stops any call made through
    another of the processor's system-call interfaces (x32 and 32-bit on x86_64), so a command cannot connect to a
    daemon's IPC socket or send a datagram. `socketpair` is allowed for a unix stream pair only, which node's
    child-process pipes use; a datagram or seqpacket pair is refused;
  - runs with a small environment of its own, without the runtime's;
  - runs under a time limit and a CPU-time limit, and anything it leaves running is stopped, and the check fails.
  A check passes when the command exits 0 and fails otherwise; its output is kept as the evidence, as before. On a
  host with no enforcing sandbox, a run check is not run at all and says "not run: this host has no enforcing
  sandbox"; it never runs unsandboxed. Such a check is not verified, which is neither a pass nor a failure: it is left
  out of the passed and total counts and said apart ("1 not verified"), the completion is not marked FAILED for it,
  the critic and the operator are shown it as not verified, it starts no repair round, and a delivery with one is not
  paid at the end of the day until a person decides. The same holds for a check the sandbox could not start for a
  reason of the host's: the kernel's Landlock too old, a limit or memory, a system path that changed, a program that
  would not start. A check that could not run because of its workspace (its directory gone, too large to copy,
  changed while it was copied, or a workspace directory that is not the one the runtime checked) fails. The sandbox
  reports its own failures, and whose reason each is, on a channel the command cannot write, so a command that prints
  the sandbox's words cannot pass its own failure off as "not run".
- A claude mind is never given a directory that reaches the runtime's state. A mission's project, or a person's grant,
  of the home directory or of the team root contains the runtime's state directories (its passcode, sessions, keys,
  the mesh node's identity, and the private directories its git reads through), and the mind was given it to read and
  write. A directory that contains a state directory, or lies inside one, is now not given, and the run says so.
- A mission's outputs are read without holding the runtime: each output's bytes, its git facts and its diff are read
  in the background, and an output over 8 MiB is listed with its git facts "not stated" (too large to compare). Before,
  every mission view read each output whole, up to 32 MiB, and ran git for it, while nothing else could run. The
  outputs of one repository are compared together, in at most six sandboxed git runs whatever their number, and a view
  compares at most 64 outputs: the rest are listed with their git facts "not stated" (too many outputs in one view).
- `/version` reads the runtime's own checkout the same way: its `HEAD` as text, and its one diff in the sandbox. A scan
  of the shipped source fails the test suite when anything but the runtime's git module starts git, and
  `xmesh mcp install` reads whether `.mcp.json` is tracked from the repository's index file instead of running git
  (an index over 64 MiB is not read, and whether the file is tracked is then "unknown", so it is not overwritten
  without `--force`).


**Fixed — missions start and stay staffed**
- A new mission no longer waits 8 s before its doer starts. Its room's operator waited for a peer in a room whose only
  peer is that doer, which was spawned only after the wait ran out. The doer now starts at once, and the mission's
  request goes out once the doer has connected, so it is still delivered live. A POST took 9-10 s; it now takes about
  a second.
- A restart hands back the claims of every worker of the previous runtime, not only the ones it killed, and offers
  that work again. Before, a review whose worker died with the runtime showed abandoned. An accept is applied once, so
  the previous runtime's accept, read again from the worker's store after the restart, no longer takes the claim back,
  however many records the run sent after it. A second worker's accept of the same work, identical in content, is
  still that worker's own and is recorded or refused like any accept. A worker's accept names the offer it answers
  (`accepts <key>@<offered at>`), so a worker staffed on the same work again under the same name (the re-offer names
  its fallback after the work) has its new accept recorded, where it was refused as a replay of the first and the work
  ran unclaimed. An accept naming an offer that has passed (the work was handed back or resumed since) takes nothing,
  however long ago it was made. The offer is read from the registry beside the worker. A worker that cannot read its
  row there names the time of its accept instead (`accepts <key>@?<ms>`), so its new accepts are recorded too. A
  replay of such an accept is refused while the row still keeps that accept (its newest 16, beside its newest 64 other
  records, so what it keeps stays bounded); and after that when its author already named a later time on the work, or
  when it was made more than 5 minutes before the work was last offered. Such an accept is refused too when the time
  it names is more than 5 minutes ahead of the runtime's clock. The allowance is for that worker's clock, usually
  another host's, either way: a worker whose clock runs more than 5 minutes ahead has every such accept refused, and
  one more than 5 minutes behind an accept made just after an offer. A replay of the first accept the work heard from
  a worker, made inside the 5 minutes before the work was offered again, once the row no longer keeps it, can still
  take the work back.
- The 10 s sweep waits until the mission store is open and the workers are restored; a step that fails is logged and
  never ends the process (`mission store not opened` after a watchdog restart). The log says what the sweep is waiting
  for while it waits, and neither wait holds it shut past 120 s: a mission store whose open hangs, or a restore that is
  stuck or never begins, lets it start and says so.
- When the runtime re-offers unclaimed work or resumes a mission, the hand-off is on the registry row before the new
  doer starts, and a queued copy of that job in the room's doer steps aside. Before, the room's doer could run a review
  the fallback was already writing, and rewrite it after delivery. While a hand-off is under 90 s old, only the worker
  it names can take the work: another worker's accept is not recorded. Every worker reads the row back once its accept
  has landed, and steps aside before doing anything unless the row names it as the acceptor, so of a room doer and a
  fallback started for the same work, the one the registry recorded does it. A worker whose accept the registry has
  not recorded within 60 s runs, as before. The re-offer no longer staffs a second doer over a worker the runtime
  handed the work to less than 90 s ago and that is still running (a restored or resumed worker slow to accept). A
  doer that is busy says so (`defers`), and the re-offer note names it: "unclaimed past 90s: <doer> deferred it
  (busy)".
- A worker the runtime stops before it accepted its job gives the job back, so the room can run it.
- A review that writes its verdict as a heading ("## 1. VERDICT" with the value below) passes its `VERDICT:` check.
  Two complete reviews were paused for a person at 5 of 6.
- Running hosted workers count as live for the supervisor, which marked their missions abandoned.

**Fixed — the runtime keeps answering under load**
- Login no longer blocks the event loop: the passcode check is asynchronous, with a small memo, and session writes
  are coalesced. A logout answers ok once the revocation is on disk, and 503 when the sessions file could not be
  written: the token is dead at once, but a restart before the next successful write would bring it back. `/version`
  reads git asynchronously and serves a cache. The registry, backlog and embedding cache are written off the event
  loop, each timed by name, a hand-off to a new doer included; only the final flush at shutdown is synchronous. Disk
  recovery grades a deliverable once per state of its files and runs its checks off the loop. The live feed derives a
  work mission again only when something in its state has changed. Under heavy host load these stalls made the
  watchdog restart the runtime.
- The end-of-day run no longer holds the event loop. At 23:00 it held it for 89 s on core-team: in one synchronous
  pass it read every node's judgement ledger on the host (192k lines) and, after each payout, verified the whole
  authority ledger again. It now reads the ledgers asynchronously, pays without re-deriving authority, and books in
  20 ms slices. On a copy of core-team's state the longest hold fell from 1.3 s to under 0.1 s, with the same lines
  and totals, measured before each booking's journal line was synced to disk. With that sync, a 1,750-booking run on a
  development Mac held the loop for at most 60 ms (50 ms without it) and took 6.7 s instead of 0.2 s: one fsync per
  booking, so the run's length, and how far a slice can run past 20 ms, grow with the volume's sync latency (about
  4 ms a sync on that Mac's SSD, and more on a network-backed volume). Two runs asked for at once run one after the
  other, so nothing is paid twice. Each booking's line is written to the run's journal, and synced to disk (as far as
  the platform's fsync goes: macOS's leaves the drive's own cache unflushed), before the booking is paid or stamped. A
  run that stops part-way (a restart between its slices) is recorded when the runtime next starts, or by the next run
  if that comes first: its breaks reach the queue, a payout it journaled and did not make is made, and one it made is
  not made again. A journal the disk refuses (a full disk) stops the run before that booking, which a later run books,
  and the run records what it booked before it as it stops; before, that booking was paid, stamped and on no run's
  record. A booking the run cannot read is booked as a break, with the reason, and not paid, and the run goes on with
  the rest. Two runs in one second, or a stopped run's record and a run started in the same second, each keep their
  own record. A run pays no booking whose end-of-day payout the ledger already holds, so a host crash that loses the
  registry's stamps after a run is recorded can put a booking on a second run's record, but cannot pay it twice. The
  journal is removed only once the run's payouts are synced to the authority ledger (once a run), so a host crash that
  loses the last payouts leaves it for the take-up, which makes them; before, it went once the registry's stamps had
  landed, and those payouts were lost on a record that said paid. A journal kept that way is taken up again at each
  run without recording any of its lines a second time. A payout the ledger refuses (a read-only or failing ledger
  volume) is owed, where it was lost once the journal went on a line that said matched and unpaid: the line says why,
  the booking keeps the debt, the breaks queue shows it, and each later run tries it again, asking the ledger first,
  until it is made or a person rules the booking: validated, the next run makes it (the verdict path reads a booking
  matched at EOD as paid there, and paid nothing, which lost it), and dismissed, the run records the refusal with the
  person's reason. A doer's membrane learns from a payout once: where a take-up or a run finds a payout already made,
  it teaches the lesson the run that paid may have died before teaching, and a membrane that has learned it learns
  nothing from it again: it keeps its newest 4,096 such lessons, two of the largest end-of-day runs, and a take-up
  teaches only what no run recorded, so a lesson comes back within about a run of its first teaching. A lesson no
  longer writes the whole agent roster synchronously, once per payout, inside the run's slices: a run's lessons share
  coalesced writes off the loop, each with every membrane's lessons and the weights they moved, and the run is
  recorded once they are on disk. On the 1,750-booking test blotter with ten membranes at that cap, the run's
  synchronous roster work fell from 150 writes of about 780 KB (0.2-0.27 s on the loop) to one 1-4 ms serialization,
  and its longest hold from 56-65 ms to 51-55 ms. A legacy calibration.json is looked for once: one that does not
  parse is said once and left where it is, where every payout read and parsed it again, and one an open refuses for a
  reason that may clear (EACCES, EBUSY) is tried once an end-of-day run.
- A line of the authority ledger that a host crash tore (its appends are synced once an end-of-day run) no longer
  empties it. Read as one piece, one such line made the whole ledger read as empty with nothing reported broken, so
  every agent's authority on the host read zero, and the next payout restarted the chain from nothing. The line is
  skipped and counted, the log says so once, the next append starts on a line of its own and continues the chain from
  the last whole entry, and the log a peer pulls carries whole entries only. An append that fails part-way (a full
  disk) is followed by one on a fresh line as well, in the same process, where the next record joined the fragment and
  the chain broke after it. A chain that does break, which excludes the entries from the break on from every authority
  read, is logged once as an error that says what is excluded and how to recover it; the torn-line message stays a
  warning.
- The embedding cache no longer holds the event loop. Every flush encoded and stringified its 42 MB file on the loop
  in one piece (5-11 s on core-team under load, 142 s at worst), and a start parsed the file in one synchronous call. A
  flush now writes the file in pieces of 256 entries, with the loop's turn between them, and a start loads it
  asynchronously, a slice at a time. The cache keeps the 20,000 vectors recall used last, where it kept the 20,000
  embedded last. `XMESH_EMBED_CACHE_MAX` lowers the cap, and a file over it loads trimmed to its most recent entries.
  On a copy of core-team's cache the longest hold fell from 110-136 ms to under 10 ms per flush, and from 310 ms to
  43 ms at load. The file format is unchanged.
- The first recall of a process scores every text the embedding cache holds. A process's first vector was cached
  under an "unloaded" kernel, and a recall whose query was answered from such an entry, before the encoder had
  started, missed the whole file and scored only the 64 texts its encode budget reached; the recall after it scored
  them all.
- Work done in slices (the end-of-day pass, the judgement read, the embedding cache's load, the record's lines) gives
  the loop its turn before its first slice too. The first slice ran at once, in the phase that started the work: after
  an await on a read, that is the poll phase, and the second slice then ran in the same turn's check phase, before a
  timer or any I/O. Two 20 ms slices were one 40 ms turn, once per file of the judgement read.
- The "advertises in the SHARED room" warning is printed once per name, not on every admission.
- Temp files a killed runtime left beside the registry, backlog, sessions file and embedding cache (one copy per kill,
  the cache's 42 MB) are removed at the next start, and one a killed `xmesh mcp install` left beside a project's
  `.mcp.json` at its next write.

**Fixed — CLI and bridge**
- `xmesh stop`, `restart` and `update` with no root named act on the runtime serving the port, and say so (macOS and
  Linux). A restart whose stop did nothing fails at once.
- Only `xmesh mcp install` and plain `xmesh` write `.mcp.json`; `help-cli`, `mission`, `status` and the other verbs no
  longer add or repair an entry where they run. The runtime itself never writes a project's configuration; plain
  `xmesh` wires the project before it starts the runtime. A `.mcp.json` tracked by git is not rewritten without
  `xmesh mcp install --force`. Writes are atomic and keep the file's mode. The `XMESH_URL` written is the runtime the CLI
  would reach (a team root's port, not always :8787).
- Joining a runtime that is listening waits for its answer instead of giving up after 3 s windows; the lease records
  when the API listens. On Windows the wait no longer runs PowerShell every second.
- With a team root named, its passcode goes only to that root's runtime, recognised by its lease pid or its environment
  (paths with spaces included). If the port's owner cannot be told, nothing is sent; `XMESH_PASSCODE` sends your own.
- `xmesh mission <id> --apply --note "…"` works, and `--note` with any other op records the note after it. A lone hex
  word that is no mission says how to make it a focus. The bridge follows a restarted runtime to its new port.
- A simulation host whose `GET /v1/host` answer is malformed is reported unavailable, with the field named, instead of
  failing later.

**Added**
- `Mission.notes` (an operator's notes, on every mission kind) and `Mission.notices` (routine mesh traffic around a
  mission: a delivery or a child request from a non-party, counted with `count`/`lastAt`). Both are add-only and
  optional in the v1 schema: the runtime always sends them (`[]` when there are none), and a Mission an earlier
  runtime returned, without them, still validates. A notice is not progress: it no longer appears in the trace as a
  failure-looking progress line. `xmesh mission <id>` and the bridge show both apart from the trace.

**Changed**
- `@sym-bot/sym` `^0.13.15`, which stops the witness storm (two signed copies of one witness relayed forever by every
  node, which filled hosts' disks and held their runtimes' threads).
- Review briefs tell the reviewer to read every source from disk in full, and to mark anything taken from WebFetch as
  INFERRED: WebFetch returns a summary of a page, and long specs came back cut short.
- Reads the SVAF provenance from the store entry as well as the record, for `@sym-bot/sym` 0.14.0, which keeps stored
  records exactly as signed.
- `.sym/node.json` is documented as the project's trusted configuration for the seat, after `XMESH_SEAT` and
  `SYM_NODE_NAME`.

## 0.10.10 — 2026-10-01

**Fixed**
- The CLI and the MCP bridge talk to the runtime of the team root you named. When `XMESH_TEAM_ROOT` (or
  `XMESH_STATE_DIR`) names a root, the runtime on the port is checked before anything is sent; if it belongs
  to another root, nothing is sent and the error says whose it is. Before, a named root could reach another
  team's runtime, be refused, and then have a mission created there with that team's passcode.
- Roots are compared by real path, so `/tmp/x` and `/private/tmp/x`, or a symlinked teams folder, are one root.
- `xmesh mission <hex-id>` reads that mission instead of creating a new one whose focus is the id.
  `xmesh mission <hex> --focus "…"` creates a mission with the flag's focus.
- In a folder set up by `sym-mesh-channel start` (0.10.0 or later), the seat is the node's name from
  `.sym/node.json`, so completion confirms reach that folder's mesh node. `XMESH_SEAT` and `SYM_NODE_NAME`
  still take precedence.

**Changed**
- The mission contract no longer pins a scenario schema version. The simulation host names the schema it
  validates against, and `capabilities` relays it as `scenarioSchema:<id>`. A client that validated against
  the old pinned schema refused fields the host accepts. `SCENARIO_SCHEMA_ID` is still exported, deprecated,
  for one release.

## 0.10.9 — 2026-09-30

**Fixed**
- `cli:codex` works: `XMESH_LLM_PROVIDER=cli:codex`, a room's `answerMind: codex` and codex lanes now call `codex exec`
  the way codex expects.
- Codex is given a model only when you name an OpenAI one. Otherwise it uses your login's default. Before, codex minds
  passed back the model from your codex config, which a ChatGPT login may not serve.
- A prompt that begins with `-` or reads `resume` reaches codex as a prompt, not as an option or a command.
- `xmesh mission <id>` checks each `CHECK:` clause in the mission's workspace, as the harness did. Before, a relative
  path showed ✗ under a passing tally.
- A question's trace says why a research step failed (every failure read "timeout") and why an answer is only
  illustrative. A refused codex call reports codex's own reason.

**Changed**
- Windows: codex can no longer be a mission's worker or critic, because its sandbox there refuses every command.
  `xmesh set mind codex` and `xmesh set critic.mind codex` are refused with the reason. A mission whose worker would be
  codex is refused before it starts, and readiness and the health line say so. Codex still works as the provider on
  Windows; use claude for work and review.

## 0.10.8 — 2026-09-30

**Fixed**
- A second `xmesh`, or a second Claude Code session, that finds XMesh still starting now waits for it to answer (up to 60 s
  from Claude Code, 120 s from the terminal) instead of reporting it failed and suggesting `xmesh stop`.
- Windows: `xmesh mcp install --user` runs the Claude CLI directly, with no shell in between. Before, a `%` or `"` in its
  arguments, such as a typed `--url`, could be expanded or end the quoting. A CLI it cannot resolve is refused, and the exact
  command is printed for you to run.
- Windows: missions find the program behind an npm launcher file correctly. Before, npm's JS launcher could resolve to bare
  `node.exe`, or to nothing.
- After `xmesh mcp install`, the CLI says to start a new Claude Code session; it no longer suggests `/mcp`.

**Still open:** `cli:codex` fails with codex 0.144.0 (see 0.10.7).

## 0.10.7 — 2026-09-30

0.10.6 was not published; its changes are included here.

**Security**
- Mission minds no longer load your own Claude Code setup: no plugins, and no MCP servers or claude.ai connectors (such
  as Gmail or Google Drive) other than the mission's own. Codex minds start without your codex config. One exception: if
  Claude Code gets its login from your user settings (an `apiKeyHelper`, the cloud credential commands, or an `env`
  block), minds keep those settings, your plugins and hooks included, so they can still log in; they still load no MCP
  servers. To see what minds did before, look for `mcp__claude_ai_` tool calls in their sessions under
  `~/.claude/projects/`.

**New**
- `xmesh mcp install --user` adds XMesh to Claude Code once, for every project.
- The MCP bridge starts a local runtime on first use (`XMESH_AUTOSTART=0` turns this off).
- `xmesh` starts the runtime in the background and returns the terminal (`--foreground` keeps it attached).
- `xmesh set`, `unset` and `settings` keep settings between starts.
- Asks are faster: store texts are encoded once, in the background, and kept (on one deployment, picking sources went
  from 47.9 s to 1.2 s once that encoding had finished).
- A review records the sha256 of the version it reviewed.
- Stall log lines show the process's CPU and memory use, and every stall of 10 s or more is logged.
- Simulation missions run on an xmesh-grid-api host (`XMESH_SIM_HOST`).
- List rows say what a waiting mission waits for (`waitingOn`), and a root-cause mission records its diagnosis on the
  mission it traced.

**Changed**
- A commission changes state only on a record signed by its author; other copies are kept as pending claims.
- A delivered file waits in `.xmesh-received/` beside its target until a person validates the work.
- A disk recovery credits no worker, and says when it only checked that files exist.
- A completion no longer marks its own outcome as validated, and next-step proposals are made only when a person asks.
- The health line says the critic uses a second model only when one is configured.

**Fixed**
- After an upgrade, a standing critic reviewed its room's whole history and then stopped reviewing.
- `XMESH_MAX_MINDS` now holds new missions back; a mission that waits gets a 202 with its place in the queue.
- Windows: `xmesh stop` waits up to 30 s for the runtime to answer, and says why when it cannot ask.
- With `XMESH_STATE_DIR` set, the CLI now finds the runtime's lease, log and passcode.
- The encoder reads "standby" until first used; workers the runtime stops are no longer logged as failures; progress is
  counted once; the live feed and the archive sweep block the event loop less.

**Known issue**
- `cli:codex` (`XMESH_LLM_PROVIDER=cli:codex`, or a room's `answerMind: codex`) fails with codex 0.144.0, which reads
  the call's `-p` as `--profile`. Use `cli:claude` or an API provider.

## 0.10.5 — 2026-09-29

**Fixed: a mission could stay unreviewed while its critic was running.** A completion reached the mission's critic
only if the doer was already connected to the critic when it completed. A doer that finished first (an echo mind on
a fresh machine did, 8 ms after accepting) left the critic nothing to review, and the mission read "no independent
review". The critic now also reads the runtime's own record of completions in its room, when it starts and whenever
that record changes, and reviews what it owes. It never reviews a review panel's lens commission, whose judgment
already lands on the mission it challenged, and it refuses completions that belong to another room.

**Fixed: a review did not move the mission.** A critic's approval or objection, and a finding disclosed while a
mission is blocked, now move the mission's `updatedAt`, so `since=` and the list's order see them. Summaries carry
the critics' tally, `review {approvals, objections}` (`{0, 0}` while a configured critic has not ruled, `null` when
none was configured), and board cards carry their newest action, `lastAction {by, at, text}`.

**Fixed: stopping the runtime could abort instead of exiting.** A process that ended while the semantic encoder's
worker was inside an inference could abort ("FATAL ERROR: Error::Error napi_define_properties" on Windows, SIGABRT
on macOS). A stop now lets the encoder finish what it started and end before the process does, and the log says how
it ended. A process that had used the encoder also could not exit on its own once its work was done; now it can.

**Fixed: a stop's drain could let new requests in.** When a stop begins, the runtime closes its listener and waits
for running missions to finish. Its listener-recovery timer re-opened that listener after 15 seconds, so any drain
longer than that accepted new requests. It no longer does, and the log says the listener closed for the stop.

**Fixed (team roots): workers broadcast what they meant to send their operator.** A worker looked for its room's
operator under a name without the team suffix that every team root gives it, so its accepts, completions and
verdicts reached the runtime only through a slower backup path. The runtime now tells each worker its operator's name.

**Fixed: issue tickets named the store that held a report as its author.** Every issue record was attributed to
the node whose store held it rather than to the author the record names. On one large deployment we measured, 945 of
2,652 tickets had the wrong coordinator. The author is now what the record claims (`metadata.createdBy`), and each
ticket member also says where it was read (`holder`) and where its author came from (`authorFrom`: `record`,
`legacy` or `none`).

## 0.10.4 — 2026-09-28

**Fixed: reading the issue queue held the runtime for up to 30 seconds.** `GET /api/v1/_/issues` rebuilt the
whole queue on the thread that answers requests: a synchronous re-read of every active store, then a clustering
pass that compared every issue with every cluster. On a host with about 5,200 issue records that held the runtime
for 25–30 s per call, and the console's mission view asked every 20 s. The queue is now read beside requests: the
stores are read asynchronously, the clustering (the same clusters, found without scoring every cluster) runs in
slices and only when the records have changed, and an answer comes from the last reading, scored at the time it is
asked. Measured on that host's stores: the same answer, field for field, and reading the queue held the thread for
at most 48 ms at a time. The answer gains `total`, `readAt` and `?limit=N`. The gateway's projection reads the same queue.

**Fixed: other routes paused the runtime on a large store.** `GET /api/v1/_/bootstrap`, read at every console
sign-in, parsed every stored CMB to report two counts: 3.4 s on that host, now 10 ms, counted from the store
listings (a count now includes a stored record that carries no categories). `GET /api/v1/_/node/:name/cmbs`
(0.56 s for a large store), `GET /api/v1/_/conformance` (2.6 s), `GET /api/v1/_/overlap` (every store on the host)
and `GET /api/v1/_/join-status` now read their stores asynchronously and return what they returned before.

**Fixed: a memory file read while it was being written left recall until the next restart.** The memory index
rejected for good any stored CMB that did not parse, and a store writes its files in place, so a read that landed
mid-write lost that CMB from recall and the pre-work brief, silently. A file that does not parse is now read again
until it has settled, and only then rejected as malformed.

**Fixed: a dropped network could stop the runtime.** The runtime's LAN advertisement (`_xmesh._tcp`) and its LAN
room discovery were made without an mDNS error callback, so a failed multicast send (a sleep/wake, an interface
change), or a multicast socket that could not bind, was thrown where nothing could catch it. They now report the
failure, at most once a minute, and carry on. With `@sym-bot/sym` 0.13.11 the runtime's in-process nodes do the
same, and one exit hook serves all of them instead of one each (every start logged a `MaxListenersExceededWarning`).

**Changed: the passcode is printed once, on the run that generates it.** Every later start read the passcode back
from its file and printed it again, so a runtime under a supervisor wrote its credential into its log at every
restart. A start that reads the passcode from its file now names the file instead.

**Fixed (Windows): `xmesh deploy-check` refuses a drive root.** It refused a team root or volume mount of `/` by
comparing strings, so `C:\` passed. Any filesystem root is now refused.

**Known issue, still open: a 202 can promise a wait that does not happen.** When the runtime is at its limit of
minds running at once (`XMESH_MAX_MINDS`), `POST /api/v1/mission` answers `202` ("created and waiting on
capacity"), but the limit does not hold a new mission's first offer, so the mission starts at once. Read the
mission's `status`, not the response code.

## 0.10.3 — 2026-09-28

**Fixed: a runtime with a large store stalled after every start, and on canon, recall and adaptation.**
Several readers rebuilt their view of the mesh's stores by parsing every stored CMB synchronously, on
the thread that answers requests. After each start, the first canon brief (a deployed agent's restore)
parsed the whole memory corpus, and the lifecycle reconcile re-read every store its oldest open mission
kept in scope. On a host with about 20,000 stored CMBs, those two held the runtime for 13.3 s and 3.7 s
after a start under load. Both now read asynchronously, beside requests. Measured on that host's stores:
the first canon brief's longest stall fell from 3.8 s to 0.15 s, and the reconcile read's from 0.74 s to
13 ms. The canon view, the adaptation routes and canon elevation read canon asynchronously too.

**Fixed: the bridge re-read the whole local peer registry once per peer every 5 seconds.** With 24 live
peers among 516 registrations, that was about 21 full reads a pass. It now reads once per pass and at
most once a second, while a process that dies is still noticed on the next lookup.

**Fixed: after a start, every mission's grounding read "nobody grounded it" for about 30 seconds.**
The first grounding reading ran before the agents and workers it reads were restored. It now waits
for them, or 60 s at most.

**The runtime names the work that held its event loop.** A stall's log line now says which periodic
work held the loop ("Held by: …"), or that none of the named work did, and a single long run is named
as it happens. `GET /api/v1/_/scheduler` reports `loopWork`, the named work with its runs and longest
run.

**A board card made from a finding names its source.** A task mission's `result.origin` is the
commission, room and artifact the finding came from, or `null` for a card a person or an agent filed.
The runtime sets it; a request cannot.

**Known issue, still open from 0.10.2: a 202 can promise a wait that does not happen.** When the runtime
is at its limit of minds running at once (`XMESH_MAX_MINDS`), `POST /api/v1/mission` answers `202`
("created and waiting on capacity"), but the limit does not hold a new mission's first offer, so the
mission starts at once. Read the mission's `status`, not the response code.

**Windows: `xmesh stop` drains.** Node turns SIGTERM into TerminateProcess on Windows, so the runtime's drain (close
the door, let in-flight missions finish within `XMESH_DRAIN_MS`, release the lease) never ran there, and `xmesh stop`
cut missions off. The runtime now serves `POST /api/v1/_/stop`: anchor only, from loopback only, refused when it
carries a forwarding header, and the lease's pid is a precondition (409 when another runtime holds the port).
`xmesh stop` on Windows asks through it and waits for the drain; the daemon releases its own lease, and its log says
so. When the CLI cannot ask (a runtime older than this release, a refused credential, a pid mismatch, or no answer
within 5 s) it says why and stops immediately, as before. POSIX is unchanged: a signal still asks. `xmesh restart`
goes through the same stop. A second `xmesh stop` during a drain waits for it rather than cutting it off: the draining
runtime marks its lease, and the second stop finds the mark. The drain's last log lines are now flushed before the
exit, so the record survives on a macOS pipe.

## 0.10.2 — 2026-09-28

**Security, Windows: upgrade Windows runtimes to this release.** Windows follows a junction or
symbolic link at a file's own name even when the write must create a new file, and any user can make
a junction. One planted at a deliverable's name made the write create a directory outside the
workspace; a file link there (Developer Mode) would have carried the delivered file itself out. The
runtime now checks the name without following it before it writes. A narrow
window remains between that check and the write, because Node cannot open a file without following
such a link. Deliverables named with a drive letter never parsed before this release. One named as
a rooted path without a drive (`/…`) did, and nobody has checked whether any Windows deployment
used one.

**Fixed: a runtime with a large store stopped answering.** 0.10.1 took the grounding tally
(`evidence.grounding`) by re-reading every CMB in every canon store every 30 seconds, on the thread
that serves requests, and the first time before the API listened. On a host with 20,386 stored
CMBs, 22 of them grounding CMBs, one reading held the runtime for 3.9 seconds at a light load; under
heavy load the readings ran back to back, the runtime stopped answering, its watchdog restarted it,
and every restart stalled the same way. Now a stored CMB is read at most once, asynchronously, and
the first reading starts after the API listens. Measured on the same stores: the longest stall
during a first reading is 21 ms, and a later reading reads no files and takes 37 ms. Until the first
reading after a start completes, `evidence.grounding` is null; `GET /api/v1/_/scheduler` reports
`grounding.ready`. `GET /api/v1/_/learning-calibration` reads through the same index.

**Fixed: `xmesh restart` could start a different copy of XMesh than the one running.** When it could
not tell which install the running runtime came from (on Windows, under load, reading the process's
command line can time out), it stopped the runtime and started the copy the command itself was run
from, detached and with the caller's environment. Now it refuses before stopping anything and says
why. Under launchd nothing changes: launchd starts its own program again.

**Fixed: missions adopted twice.** On the second start after upgrading to 0.10, a world's other
commissions (a critic's, a replay's, a re-post) were adopted again as missions of their own, beside
the world's mission that already carries them. Adoption now counts every commission of an adopted
world as carried. A start removes the duplicates it finds, but only while nobody has touched them:
never updated, no children, no verdict, fault or abandonment. It keeps anything a person or client
acted on, and the startup log says how many went.

**Fixed: a completed mission says when it ended.** `endedAt` was null for a completed work mission
and for a finished board card. It is now the time the commission completed, or the card's move to
done or dropped, whenever the record itself does not say.

**Fixed: a review that never happened no longer looks like no review configured.** A review or
assessment records whether an independent critic was configured when it started, and its
`evidence.review` is then present. With no verdict, it means the critic has not ruled; once the
mission has completed, it did not, so the mission is unreviewed, never passed. Health and the README
now say what verification does: each work, review, research and assessment mission gets a critic
on a second model. A critic that cannot run leaves its mission unreviewed.

**Fixed: the MCP bridge keeps its seat when its install moves.** When the bridge path in a
project's `.mcp.json` no longer exists (a global reinstall after a node upgrade, say), `xmesh`
repairs it. The repair rebuilt the whole entry: it dropped `XMESH_SEAT`, so the bridge went
anonymous, and it replaced the file's `XMESH_URL` with the caller's. It now changes the path and
keeps the rest of the entry.

**Windows: missions deliver, checks run, and the repository builds.** A deliverable named with a
drive letter (`C:\…` or `C:/…`) is now read, so its file lands. `CHECK: run` clauses execute in the
platform shell instead of `/bin/sh`, which Windows does not have. `npm ci`, `npm run build` and
`npm run test:integration` work on a stock Windows machine, and the deployed-version guard no
longer aborts as it exits.

**A test-only route for app UI tests**, served only by a daemon started with `XMESH_TEST_HOOKS=1`:
`POST /api/v1/_/test/drop-sse` closes every live event stream, so an app's reconnect can be tested.
It is anchor-only. Without the flag it does not exist. With it, the daemon says so at boot and
health reports `testHooks: true`.

**Known issue: a 202 can promise a wait that does not happen.** When the runtime is at its limit of
minds running at once (`XMESH_MAX_MINDS`), `POST /api/v1/mission` answers `202` ("created and waiting
on capacity"), but the limit does not hold a new mission's first offer, so the mission starts at
once. Read the mission's `status`, which never says it is waiting on capacity, rather than the response
code.

**The README says what a runtime does on the network**: it joins your machine's SYM mesh room as an
observer and announces itself on the LAN (`_xmesh._tcp`). `XMESH_NO_BRIDGE=1` and
`XMESH_ANNOUNCE=0` turn each off. It also says that on Windows, `xmesh stop` ends in-flight missions
at once.

## 0.10.1 — 2026-09-28

**A mission list row carries what a client shows.** Each `MissionSummary` now also has `reportedBy`,
`checked {passed, total}`, `parent`, `firstArtifact`, `completionCmb` and `grounding`. All are already
stored or indexed, so a page of the list still runs no CHECK clause and reads no output from disk. A
client no longer reads every mission in full to draw its list.

**Whether a mission held up is back:** `evidence.grounding {verified, failed, lastAt}`, the tally of
grounding CMBs that reach the mission, under the rule that an untrusted peer's `failed` never counts.
0.9 carried it on every commission; 0.10.0 dropped it along with the route that computed it. The
runtime now takes the reading in the background every 30 seconds.

**The list refuses a parameter it does not know** (`422 validation_failed`, naming it) instead of
ignoring it, so a filter a runtime lacks can never read as a match.

**Find the mission a CMB belongs to:** `GET /api/v1/mission?cmb=<key>` returns the mission whose
request CMB or completion CMB is that key. The completion's CMB is now on the mission too, as
`evidence.reported.cmb`: null when the runtime recorded none, as for a completion recovered from disk.

**Board cards carry the card.** Each card in `view=board` has its `owner`, `size`, `rank` and
`blockReason` beside its summary. Its actions and reviews stay on the mission's own read.

**A simulation run can be `unjudged`**: it played to its end, and the engine gave no judgement.
Either the world has no mission, or its deadline lies past the run's `secs`. `unjudged {reason,
deadline}` says which. It is never a win or a loss, and `byArm` counts it apart. Simulation is
still not served by this runtime (501); this is the contract a host will speak.

**Fixed:** abandoning a mission no longer reads as a person dismissing it. An abandoned mission
carries the verdict a person gave it, or none. The contract's status line no longer says
"design for review", and its route table lists `/api/v1/_/issues/fit`.

## 0.10.0 — 2026-09-28

**Breaking: the runtime has one public API, `/api/v1/mission`, and every older route is gone.** Every
request is one mission of one kind: `work`, `question`, `review`, `research`, `assessment` or
`task` (`simulation` is in the contract and answers `501 kind_unavailable` until a simulation host
is configured). `POST` hands one over, `GET` reads your missions, a view of the collection
(capabilities, board, register, recall, canon), one mission, one part of it, or its live feed,
`PATCH` operates on it (a verdict, resume, replay, apply, a card's claim or start, and so on) and
`DELETE` abandons it. One request makes one mission: the same ask twice is two missions, and a
retry that must not duplicate sends an `Idempotency-Key`. Every refusal has one shape. The contract
(`docs/api/mission-v1.md`) and its JSON Schema (`schemas/v1/mission.schema.json`) ship in the
package, and `GET /api` on a running server says where to start. The runtime's own routes (session,
health, readiness, version, the live events and the operator's settings) moved under `/api/v1/_/`:
they are plumbing for its own clients, not an API to build on. **Point any health monitor at
`/api/v1/_/health`**; the Docker healthcheck and the Kubernetes probes in this package already do.

**One MCP tool: `mesh_mission`.** Its `action` says what it does: `create` a mission of any kind,
`read` one by id, `list` yours, `operate` on one, `abandon` one, or `view` the board, the mesh's
memory (`recall`), its canon or this deployment's capabilities. `mesh_ask`, `mesh_review`,
`mesh_missions`, `mesh_board`, `mesh_recall`, `mesh_canon` and `mesh_lanes` are gone: a question and
a review are kinds of mission, and every read is an action. Because it is one tool, a client that
allows it without asking also lets it start missions; leave it on "ask" to approve each one. The
server's instructions to the agent are rewritten for plain reading. A connection with no seat is
told, on the mission it hands back, that nothing will notify it.

**One CLI verb acts: `xmesh mission`.** `ask`, `offer`, `lint`, `detail`, `rule`, `replay`, `abandon`,
`register`, `review`, `assess` and `research` are folded into it; `xmesh missions` lists.

**Abandoning a mission stops its work.** A world of its own is torn down, a commission filed into a
room is dismissed (a late completion cannot book), a task's card is dropped, and a question starts
no further mind call and records no late answer. Abandoning an abandoned mission returns it; a stop
that fails is recorded on the mission, and abandoning it again retries.

**A capability request says what a person decides and what approving grants.** Beside the blocked
agent's own words, kept for the record, each request carries the plain-language question, a
recommendation with its one-line reason, whether it is a yes-or-no at all, and what approving
actually does, which can be broader than what was asked for. The runtime derives all of it, so every
client shows the same decision. A mission waiting on approval shows the questions, and a request
that has to be set up rather than approved is offered no approval.

**A verdict says whether it reached the author's memory.** Beside who ruled and when, a verdict now
records whether it moved the weight in the author's own store, the effect later recall reads.

**Operations on one mission run one at a time**, so two people starting the same card make one
mission, and starting a card or asking for a root cause reads the spend cap before anything is
created.

**Upgrading.** A 0.10 CLI and MCP bridge recognise a 0.9 daemon, so `xmesh update` from 0.9 works
and the bridge says which side is older. A 0.9 bridge cannot use a 0.10 daemon: restart your MCP
client after upgrading. Anything built on the old routes must move to `/api/v1/mission`; the
contract's §13 maps each retired route to what replaced it.

**The CLI ends by exit code.** On Windows, a refused mission could crash the CLI (exit 3221226505)
after printing the refusal, instead of exiting 1.

**`XMESH_LLM_PROVIDER=echo` now means no real model anywhere**, including a hosted agent's thinks, a
room's answer mind, a local model, a lane probe (saving an API lane no longer reads its key or asks
the provider) and a worker whose lane, room policy or critic names claude or codex, so an evaluation
or test runtime cannot reach the operator's own login or keys.

## 0.9.50 — 2026-09-26

**A runtime with a large store starts in seconds instead of timing out.** At boot the runtime restores
its deployed agents, and each restore read every stored record synchronously to build its canon. On
a store of 37,283 files that held the process for minutes: it bound its port but could not answer,
its watchdog killed it, and it restarted into the same wall. Canon is now read asynchronously, in
batches, once per restore of all agents rather than once per agent — measured on the live store,
10.5 s with the event loop held throughout became 2.7 s with no stall longer than 39 ms, and both
produce identical canon. The memory store underneath (`@sym-bot/sym` 0.13.9) builds its index the
same way.

**Admission no longer treats disagreeing memory as empty memory** (`@sym-bot/sym` 0.13.9,
`@sym-bot/xmesh-core` 0.3.1). A record that contradicts what an agent already holds, or meets a small
category weight, used to be admitted as though the agent had no memory of the subject. Whether a
category can be judged now depends on how much live memory carries it; memory that points the other
way scores full drift; a category weight of 0 disables that category instead of counting as full
weight; and weights that are not finite, negative, or all zero are refused.

**The lineage anchor is resolved from records this runtime stored** (`@sym-bot/sym` 0.13.9). The
check that decides whether a remix keeps its lineage no longer takes the sender's ancestor list or
the author's clock as input.

**The room roster says what the room admits, not only that it is gated.** `gated: true` describes the
pinned owner; the roster now also carries `admits` and `admitsWhy`, and says plainly when a gated room
admits nobody — its state in every shipped runtime today, because a peer's key cannot yet be proven
at handshake — and that members listed then predate the gate rather than satisfying it.

## 0.9.49 — 2026-09-16

**`xmesh update --check` could not work on Windows at all, and said the registry was unreachable.**
A Windows seat was told the npm registry could not be reached while `npm view` succeeded in the
same shell a second later. The cause was a spawn that never ran: npm is `npm.cmd` on Windows and
Node refuses to spawn a `.cmd` without a shell, so the command failed before it reached the
network and then blamed the network. The diagnosis was wrong in the most misleading direction.
Fixed, and a failed spawn now names itself rather than pointing at your connection.

**Four checks reported "nothing is there" when the truth was "I could not look."** Each failed
open, and each was found by running the code on Windows rather than reasoning about it.

- The port-ownership guard shelled out to `lsof`, which does not exist on Windows. The spawn
  failed, the parse produced nothing, and the caller read that as "no conflicting process" — so
  the staleness guard that stops `update` reporting another tenant's version was silently skipped
  on every Windows host. It now uses a structured query there, and when no method can answer it
  says so instead of passing.
- Resolving which package a running runtime uses shelled out to `ps`. On Windows that failed too,
  so "installed" fell back to the CLI's own version — the exact "a stale seat reads as current"
  failure 0.9.48 fixed one level up. It now reads the process command line properly. A command
  line that comes back empty because you do not own the process is reported as unreadable, not as
  "not xmesh".
- A `daemon.lease` written by PowerShell carries a UTF-8 byte-order mark, which JSON rejects, so a
  present and well-formed lease was reported as "no lease at …". The mark is stripped, and a file
  that exists but does not parse is now distinguished from one that is not there.
- A three-second version probe timing out on a loaded host printed "not running" about a daemon
  that was demonstrably alive. It now says the lease is alive and holding the port but did not
  answer in time, and names `XMESH_PROBE_TIMEOUT_MS`.

**`xmesh restart` could leave a machine with no runtime.** It started the replacement from the
code of whichever CLI invoked it rather than from the package that was running — so running it
from a source checkout silently swapped the running release for that checkout, and if the checkout
was unbuilt, for nothing. It now restarts the package that is actually running, and says which one
that is when it differs. More importantly it checks that the target can boot BEFORE stopping
anything: refusing early costs a message, refusing late costs the runtime.

**`xmesh stop` promised a graceful drain on Windows and could not deliver one.** Windows has no
signals — the stop is a hard terminate, so the drain that lets in-flight missions finish is
unreachable there. The command said "in-flight missions finish first" anyway. It now states what
actually happens on the platform it is running on: on Windows, work in flight is cut off rather
than drained, and resumes on the next start. Making the drain reachable on Windows needs a
cooperative shutdown channel for a detached daemon; that is not in this release. A clean stop also
no longer leaves its lease file behind, which was making the next `update --check` report a
runtime that had been stopped properly.

**A mission room can now say who is in it.** `GET /api/rooms/:name/members` returns each member
with the evidence for its membership — started by this runtime, an operator, a resident, or a peer
that declared the room at its own handshake. It reports whether the room is gated by asking the
node rather than assuming, and distinguishes "ungated" from "could not determine": an empty
invitation list under a false "ungated" reads as "nobody was admitted" when the truth may be
"nothing was required", and those call for opposite responses.

**Verification.** The Windows behaviour above was measured on Windows 11 by another seat, one
command at a time, including the failures: eleven items covering the spawn, both port lookups, the
process lookup, the stop message, the lease handling, and both arms of the restart refusal. Two
things are stated rather than claimed: the Windows drain does not exist, and no released package
had these fixes before this one, so an installed Windows seat upgrading to 0.9.49 is the first
time they reach a real install rather than a checkout.

## 0.9.48 — 2026-09-14

**`xmesh update` reported another tenant's version as your own.** dev-team-4 caught it within an
hour of 0.9.47, on the seat the release note had just named as furthest behind: `update --check`
said "running 0.9.47" while its daemon was 0.9.46 and had started two days before 0.9.47 existed.
The cause was a stale lease plus a guess — a lease file naming a pid that was alive but holding
something else, on a port a different tenant's runtime had since taken, and a fallback to `:8787`
whenever no lease resolved. The probe answered honestly and the answer belonged to a stranger. It
failed in the direction that matters: telling a stale seat it was current.

A lease is a claim, not evidence. It now holds only if its pid is alive AND that pid is the process
listening on the port it names; otherwise there is no runtime for that state root and the reason is
printed — the pid, the port, and who really holds it. Nothing falls back to a default port.

**The upgrade path was wrong for per-seat installs, not just the report.** Two topologies live on
one host: a runtime running from the global install is updated by `npm install -g` for free, and a
runtime running from `~/.xmesh-teams/<seat>/runtime` is never touched by it. A seat on the second
could follow the release note exactly, see a green check, and stay behind for ever. "Installed" now
means the copy the RUNNING process runs, resolved from its own entrypoint, and the install goes to
that same place: `--prefix` for a seat tree, `-g` for the global, and a refusal for a source
checkout, which is updated with git and never by npm writing over it. A successful update also says
that the coding-agent session needs its own restart, because a runtime update never reaches the MCP
bridge: tree, then job, then session.

## 0.9.47 — 2026-09-14

**`xmesh update` — and a runtime you can restart without bravery.** Re-running
`npm install -g @sym-bot/xmesh` was always the update; what it could not do was move the RUNNING
runtime onto the new code, and nothing said so. The only stop was reading a pid out of a lease file
and killing it, which killed a doer mid-mission and left its commission accepted forever. So
installs drifted: two tenants on this host sat eight releases apart. `xmesh update` reports
installed against the registry against what is actually being served, installs, restarts, and then
verifies by asking the runtime rather than the checkout — a tree can lie, the endpoint cannot.
`--check` looks without changing anything. `xmesh restart` and `xmesh stop` are the same drain with
and without the start: the door closes first, in-flight missions finish, workers are stopped, the
lease is released. `XMESH_DRAIN_MS` bounds the wait at 90 s, past which it stops anyway and says
what was still running.

Three defects surfaced while building it. The drain was **dead code**: `hosted-workers.ts`
registered SIGTERM at import time and exited at 143 before the drain could run, so a signal
produced a dead process and a log with no drain line in it. The exit is now claimable —
`ownShutdown()` makes that handler yield and the owner stops the workers itself once drained;
nothing changes for any caller that has not claimed it. Restart **raced itself**: a dead pid is not
a free port, so the replacement found the old socket and exited, leaving a pid that had joined and
gone while another held the lease; it now waits for the port. And on a supervised host a **service
manager silently undid every stop** — a launchd agent owns the daemon and restarted it seconds
later under a new pid, so the command reported success and changed nothing. Both verbs now find the
job holding the lease and go through it: `kickstart -k` to restart, so the drain still runs and no
loose second process appears, and `bootout` to stop, printing the line that brings it back. systemd
is owed, not pretended: off macOS the loose-process path is used.

**A needle found only where the artifact quotes the check is not a match.** A mission carrying
`CHECK: contains <file> NOT reachable` passed on a deliverable whose subject sentence said the
opposite, because the doer honestly quoted the clause to explain which check its finding would
fail. The green was bound to the doer's transparency: a truthful negative passed the check built to
detect it, and writing the expected phrase would have scored the same. A line that restates a check
clause is no longer deliverable text, and a needle living only there fails with "the criterion was
echoed, not met". `not-contains` reads the same filtered body. An artifact whose real content
includes check clauses still passes on any occurrence elsewhere.

**The version-skew warning no longer outlives its reading.** The daemon's version was read once per
bridge process and cached for the life of the connection, so a bridge alive across an upgrade kept
naming a version that no longer existed — one seat saw the same wrong number for eight days, and
this runtime's own bridge printed another. The reading now expires (`XMESH_SKEW_TTL_MS`, 30 s), so
the banner follows the daemon and clears when the skew is gone. `mesh_lanes` never carried the
banner at all — the one view whose subject is what a deployment can run could not say it was being
read through stale code.

**An ask's `project` is honoured for validator and anchor principals only.** 0.9.46 let any
authenticated principal name an absolute directory on the ask; the doer received it as
`--add-dir` on a mind that runs with edits pre-approved, so naming a directory was write authority
over it. On the developer runtime the one passcode is the anchor, so the Claude Code path is
unchanged. On a team root, a participant — a per-user credential the anchor issued, the shape a
consumer device would hold — now gets the mission without the project and a note saying so
(review mission-44692a, finding a-2). Noted, not an advisory: in 0.9.46 the only principals were
the anchor and users the anchor created, and the anchor could already grant such writes.

## 0.9.46 — 2026-09-05

**Several Claude Code sessions, one runtime — verified.** Two sessions in two projects filed
missions at the same instant against one runtime: both accepted together, completed 2/2 in 30 s
and 40 s, each deliverable in its own repository, each recorded under its own session seat. One
runtime per user serves every project and session on the machine. A seat is attribution and
routing, not a security boundary: every session holds the deployment's one passcode, sees the
deployment's missions and shares its budget. One person, many sessions: one sovereign — which is
the default use case. A shared team runtime with per-person boundaries needs per-seat credentials,
a named design item, not this release.

**A Claude Code user needs no setup beyond `npm install -g @sym-bot/xmesh` and `xmesh`.**
Measured on a fresh install of the published 0.9.45 into an empty prefix, on a host whose PATH
carried `claude` and no `codex`: install 12.7 s, first start generated the passcode and wrote the
project's `.mcp.json`, the doer and critic both resolved to the claude CLI with nothing declared,
and a first mission completed 2/2 checks in 27 s. Two things stood between that and "no other
setup": the README told a Claude Code user to declare `XMESH_LLM_PROVIDER` and to run
`xmesh mcp install --seat …`, neither needed; and the bridge warned at every boot and every
mission that it had no seat. Now the bridge derives its seat inside a Claude Code session from
the two variables Claude Code hands every MCP server (`claude-<project>-<session>`, byte-identical
to the SYM mesh plugin's node name there, so completion confirms reach that plugin when it is
installed). A pinned `XMESH_SEAT` still wins; outside a Claude Code session nothing is invented.
`mcp/seat.mjs`, tests in `test/seat-derivation.test.ts`.

**A mission asked from Claude Code knows which repository it is about.** Measured on the packed
0.9.45 with the README's own example, "review this repository", sent through the bridge from a
project directory: the doer, launched in its mission room, searched the state root and the whole
temp tree for a repository, found none, and blocked after 95 s. The bridge now sends the session's
project directory (`CLAUDE_PROJECT_DIR`, else its cwd; a `project` argument overrides) with every
ask; the door verifies it is a directory this runtime can see and writes it as the first line of the
ask — `PROJECT: <path>` — which the doer, the critic and the check runner all read. Relative
`CHECK:` paths resolve in the project when they are not in the room workspace, and `CHECK: run`
executes there. The doer is granted the project directory for writing (it is the ask's own
target, not a widening); a project the daemon cannot see is noted on the reply and ignored, never
refused. Measured on the packed build, same example: the doer read the repository, wrote
`risks.md` into it with three cited findings, checks 2/2, in 185 s — and said in the file what it
could not verify, because the run's tool allowlist denies executing project code.

**A second `xmesh` joins the running runtime instead of refusing.** Run `xmesh` in another
project while one is already running and, when that runtime answers, this project's `.mcp.json`
is wired to it and the command says so and exits: one runtime per user serves every project. The
refusal remains for a holder that is alive but not answering — a fault to surface, not a runtime
to point at. Previously the second run printed a state-root refusal, wired nothing, and advised
stopping the daemon.

**The worker records the lane it resolved.** At spawn: `run lane — <lane> · transport=<cli|api> ·
mind=<mind> · model=<model> · <provider host>`; the staffing line carries the lane and model
arguments. The evidence of which model a run used is this record and the provider's usage page,
never the doer's own statement — a critic caught a proof mission asserting its model by echo.

**Package metadata:** `keywords` and `author` set (the package was findable only by name);
description and README no longer say "human authority" — the ruling route is authenticated, not
role-gated, so the copy says what the runtime records: acceptance is a separate recorded act.

## 0.9.45 — 2026-09-04

**A mind could read the daemon's secrets. Fixed. Read this first; the rest of the release is
below it.** Through 0.9.44 every mind the worker spawned inherited the worker's whole
environment, and the worker inherited the daemon's. So the deployment passcode
(`XMESH_PASSCODE`), the relay token (`SYM_RELAY_TOKEN`), the licence (`XMESH_LICENSE`), the Z.ai
key (`XMESH_GLM_API_KEY`), an API key set for the HTTP provider (`XMESH_LLM_API_KEY`), the other
vendor's CLI key, and anything else an operator placed in the daemon's environment were present in
every mind's process. Any child of the mind that reads an environment could read it. No claim is
made here about what gated that on any platform: a codex doer runs in a sandbox that has a shell
by design, and on a Windows claude doer with no operator grant the doer attempted a shell tool
call — whether it executed is not established, so this note carries no mitigation. It was found by
a mesh review mission run against SYM.BOT's own shipped source, before any report from an adopter.
No evidence of exploitation was looked for and none is claimed; the honest statement is that
nobody checked, not that nothing happened.

**What to do, in this order.** (1) Rotate any model API key that sat in the daemon's environment
on a host where missions ran — `XMESH_GLM_API_KEY`, `XMESH_LLM_API_KEY`, a vendor CLI key — first,
because a leaked model key is someone else's invoice. (2) Upgrade to 0.9.45. (3) Rotate the
passcode and the relay token; the licence is a signed value, rotate it with a new activation if
your operator issues one. Upgrading does not un-expose a credential that was already readable.

The fix: every mind is spawned with an environment built explicitly (`childEnvForMind`,
`src/lanes.ts`) — the runtime's secrets and every OTHER lane's credential removed, with only the
variable family the mind's own CLI authenticates with kept (`ANTHROPIC_*` for claude,
`OPENAI_*`/`CODEX_*` for codex). A pointer to a secret counts as one: `XMESH_LLM_KEY_FILE` and
`XMESH_LICENSE_ACTIVATION_PATH` are stripped too, because a mind that can open a file can read
what they point at. The camel-dryrun harness is spawned with an allowlist of nine variables.

**And the class is being removed, not only the instance.** A model key should not sit in an
environment variable at all (founder ruling, 2026-09-04). A lane's credential is now read from its
`keyFile` at spawn, and an integration's from its `credentialFile` at the moment of the call —
`credentialEnv` names the variable the command expects, not where the value lives. With a file,
the daemon's environment holds no model key, so there is none to leak, to scrub, or to forget to
scrub. Taking the value from the environment still works for deployments that predate the ruling
and warns when it does.

**Two limits, stated rather than implied.** The kept family necessarily holds a credential: a glm
lane's Z.ai token is copied into `ANTHROPIC_AUTH_TOKEN` because that is how the claude CLI
authenticates, so a mind can read the key it runs on. That is structural to a CLI mind. And the
deny list is declaration-driven: a vendor key an operator leaves in the daemon's environment that
no lane and no integration names is not recognised as a credential and passes through. A
shape-based default-deny would close it and would also strip keys a granted tool legitimately
needs, so it is a design change for review, not a quiet widening in a patch release.

Tests plant sentinel values for every named secret and assert none reaches a claude or codex
child, that the key-file pointers are stripped, and that no spawn in the worker inherits
`process.env`.

Affected: **every published version, 0.1.0 through 0.9.44**, on every platform — measured, not
assumed: the pattern entered with the first worker (commit b07ceaf, 2026-07-04), the twelve
earliest published tarballs were unpacked and each carries it at five sites, and no later commit
removed it before this one.

---

**The model table is live, editable, and shared how you choose.** A lane — where a mind's traffic
goes and whose account pays — used to be frozen in an environment variable at boot. There is now a
saved table the runtime re-reads at dispatch: `GET/PUT/DELETE /api/lanes`, a Models panel in
Station, and a `mesh_lanes` tool. A lane names a credential and never carries one, so the table is
safe to show and to share: point `XMESH_LANES_SOURCE` at a file (a synced folder, a git checkout)
or at any URL answering `GET`/`PUT { rev, lanes }` and this deployment mirrors it. CLI lanes are a
person's own login and stay local; API lanes are the team's and travel. A worker now resolves its
lane **per run**, so an edit takes effect on the next mission with no restart, and a lane that can
no longer carry the run is refused with the remedy named.

**The xagent decides what a task needs; the runtime tells it what the mesh has.** Every doer gets
`lanes.md` at hand-over listing what is declared and what is **callable by you now** — declared is
not callable, and reporting otherwise let a host accept work it could not perform. If the task
needs a capability nothing can serve, the doer stops with `BLOCKED: no lane declares <capability> |
owner: requester | closes: …`, which is broadcast to the room for a volunteer first; hosts that
cannot serve it stand down instead of taking it. Measured on one identical request: 12m19s to a
vague refusal on 0.9.34, 69s to that block on this build.

**The integration layer: an xagent's only door to the outside.** A second table names SYSTEMS — an
ERP, a database, a microservice, a renderer — with the operations the layer will perform and the
NAME of a credential. The worker starts a credential-holding service; the mind's CLI gets a proxy
holding a per-run token. Read and write are separately-named tools so the tool allowlist carries
the gate. Every call is audited to the workspace. **Never run against a real system yet.**

**A resumed mission continues from where it stopped.** `resume-context.md` carries why it stopped,
what cleared it, what was reported done and what is on disk.

**A dead mission can no longer take credit for someone else's file.** The disk-recovery path that
rescues a worker which delivered and died silent no longer admits a mission that reported it was
blocked, requires a paused mission's artifact to predate the release of its crew, and moves its
floor on a resume. What is not closed is stated in the code: the guard binds an artifact to a
time, not to a writer.

**Every review must show its work.** A review this runtime issues now demands, in the brief and in
its own acceptance criteria, that each ground be labelled DERIVED — with the file and line, or the
command and its output — or RESTATED. Whether the reviewer showed its work is recorded beside the
judgment, never merged into it.

**The board can no longer be down in silence.** A liveness watchdog runs outside the daemon's
event loop, probes its port, and restarts it after a bounded run of failures. The daemon's three
previous recoveries were all inside the process that could be stuck.

Also: a definition of done is prose unless it is a check the grammar can read (three built-in ones
were unparseable and failed every fork minted from them); HTTP 402 and the vendors' balance
wording now classify as credit, so an exhausted personal key pauses the pool instead of grinding.

## 0.9.44 — 2026-09-04

**Windows could not run a mission at all, and the reason was one missing option.** A cold seat on
Windows 11 installed the published 0.9.43, started it, filed a mission, and watched the doer accept
and block in 20 milliseconds: `stopped: spawn claude ENOENT`. Root cause measured on that machine
rather than inferred — a globally installed CLI on Windows is a `.cmd`/`.ps1` shim, not an
executable, and Node on win32 cannot spawn one without a shell; `spawnSync("claude", ["--version"])`
fails ENOENT where the same call with `shell: true` returns the version. So no mission needing an
LLM mind could execute on Windows, which is every mission that is not `echo`. All three CLI spawn
sites now go through one helper that adds a shell **only** on win32, **only** for a bare unpathed
name. **No shell is used on any path**, and that is the whole point: the first attempt at this fix
set `shell: true` on win32, and a Windows execution measured what that does — of eight brief-shaped
arguments, 0 of 8 survived. Every multi-word argument is truncated at the first space, so "review
this repository and identify the top three release risks" arrives as "review"; an argument
containing ">" arrives EMPTY, because the interpreter performed a redirection and created a file
named from the brief's own text. That would have turned a loud, harmless ENOENT into a doer that
spawns happily and answers a question nobody asked. Two other routes are also dead and are recorded
in the source so they are not re-derived: spawning the `.cmd` by explicit path returns EINVAL (Node
has refused that since the CVE-2024-27980 hardening), and `cmd.exe /c` mangles identically, because
anything routed through a command interpreter re-parses the text.

What works is resolving the shim to what it actually wraps and running that with the argument array
untouched. **Both shim shapes are handled**, which is the second thing only a Windows box could
teach: the cmd-shim template wraps a script, but npm may install a native launcher instead — the
real Claude Code shim on win32 names a `.exe`. A script runs under node; an executable is spawned
directly. Verified end to end on win32 (node v24.14.1): the real binary executes and all eight
brief-shaped arguments arrive byte for byte, the `">"` included. Same family as the
`import.meta.url.pathname` defect of 0.9.14 — a POSIX assumption invisible on darwin.

**A mission told an agent it was complete when it had delivered nothing.** A blocked commission
BOOKS, so a mission whose every commission blocked reads `status: "complete"` — correct for the
lifecycle, and a lie to whoever polls it. That was known, and the mitigation was to carry `outcome`
beside `status` in every mission view; the MCP bridge dropped the field, so the surface every coding
agent polls still reported a completion for a run that produced no artifact and whose critic never
ran. The outcome now travels with the status there, and when a mission reads complete without
delivering, the row says so in words rather than leaving two fields to be reconciled.

**A worker routed to another vendor came back on the operator's own account.** A `glm` worker is
persisted with mind `claude` — that is its mind — and the vendor it was routed to lives in a
separate `lane` field. `restoreWorkers` typed the roster row without that field and never read it
back, so a daemon restart silently moved a Z.ai worker onto the operator's Anthropic subscription:
the one outcome the spawn path refuses by name, enforced at spawn and lost on restart. The lane is
read back now. Two comments claimed the vendor translation happened at one site and nowhere else
while a second copy sat in the ask path with its own default endpoint, so moving
`XMESH_GLM_BASE_URL` moved one caller and not the other; both call one function now. A restored
critic also re-resolved its spec from the deployment alone, losing a room's own critic mind on
restart — it takes the room's policy again.

**A blocked step now carries its whole question.** The stop text carried the first 100 characters of
the doer's finding, and 100 characters lands mid-sentence — before the `| owner: | closes:` grammar
the doer had just written. On a real block the cut fell one word after "renamed" and never reached
"or a separate agent", which is the half that made it a decision rather than a question of fact; an
operator and a seat both read the fragment and answered the wrong question. The question is carried
whole, and where a finding uses the register grammar the two fields a reader needs — who holds it
and what would close it — lead the sentence instead of being cut off the end.

## 0.9.43 — 2026-09-03

**The image no longer carries apt.** Today's scan of the release candidate raised a new HIGH
finding with no upstream fix (CVE-2026-16742) in `libsystemd0` and `libudev1`. Their only
remaining dependents on the slim base were `apt` and `libapt-pkg6.0` — an installer that never
runs in the image — so all four are purged in the Dockerfile, the way earlier base-package
findings were removed rather than excepted. The build still proves the neural path, TLS, the
shell, tar/gzip, the codex CLI and `xmesh deploy-check` on the pruned image before it is scanned.

**A mission's grounding lands where its doer runs.** `POST /api/missions` writes the mesh's
retrieved context (`canon-context.md`) for the worker to read first — and wrote it to a
caller-supplied `workspace`, or by default to the team root every room shares, while the doer it
staffs is launched in the room's own directory (`<team root>/work/rooms/<room>`). Measured on the
published 0.9.42 with a workspace outside the team root: the doer found no such file and blocked
with nothing delivered, as its brief required. The file is now written to the room's directory at
both grounding sites (missions and accepted next-moves), so "in your workspace" is true; a
`workspace` field on the mission door is refused by name with the room's directory in the message,
instead of accepted and misrouted. Write deliverable paths under that directory.

**The deployment's environment is readable before it is deployed.** `xmesh deploy-check` reads
the environment it runs in and reports what the runtime would do with it: the `team-…` id a
licence binds to, printed *before* the first boot instead of discovered from the boot log; an
explicit `XMESH_STATE_DIR` or `SYM_STATE_DIR` that overrides the team root instead of
reinforcing it; `XMESH_TRUST_PROXY` as the hop count it is; whether the declared mind's CLI is
on the PATH of the process running the check — and it says so, so a container is checked by
running it inside the image. Exit 1 on a fault, `--json` for scripts. It never reads a secret's
value. Harvested from a real deployment package whose preflight had to guess at each of these.
Two things the check found are fixed in the runtime as well: a team root written with a
trailing slash predicted one licence id and bound another once the directory existed — the id
is now derived from the normalised path, so the prediction and the binding agree; and a
non-numeric `XMESH_TRUST_PROXY` (`true`) was silently ignored, leaving every client address
read as the proxy's — the server now refuses it by name at boot.

**The Kubernetes manifest no longer binds a licence to a root that owns no stores.** Run over
its own environment, `deploy-check` failed the shipped manifest: it pinned `XMESH_STATE_DIR`
and `SYM_STATE_DIR` under the volume while telling the operator to uncomment `XMESH_TEAM_ROOT`
beneath the same volume — and an explicit store path wins over the root. The volume now mounts
at `/state`, the image's own layout: with no team root the entrypoint places both trees there,
and the licence lines to uncomment are `XMESH_TEAM_ROOT=/state/teams/<team>` with
`XMESH_VOLUME_MOUNT=/state`, which the entrypoint validates and stamps. The manifest's
environment is replayed through `deploy-check` in the test suite, in both tiers.

**One vendor key now reaches both of the container's minds.** Measured on the 0.9.42 image
with the README's own `api:openai` + `OPENAI_API_KEY` flow: the entrypoint logged the codex CLI
in — the worker mind — and unset the key, and the server process then carried no key at all
for the runtime's own completion (asks, agent think, synthesis), which reads
`XMESH_LLM_KEY_FILE` or `XMESH_LLM_API_KEY`; every such completion failed with "no key". The
entrypoint now hands the vendor key to the runtime the way the runtime prefers — a `0600` file
read per call, never the long-lived process environment — and only when the operator named no
key source themselves; `ANTHROPIC_API_KEY` gets the same hand-over for `api:anthropic`.
`deploy-check` reads the entrypoint's presence to tell the two cases apart: inside the image the
vendor key alone passes, natively it fails with the fix named. Two more faults from the same
measurement: the image had no `xmesh` on its PATH — npm was purged after the install and the
package's bin link never rode the copy — so the README's `--entrypoint xmesh … deploy-check`
example could not run; it is linked now and proved in-build. And the generative ask's think and
compose seams opened only on a claude login (`which claude`), so a container declaring
`api:openai` or `cli:codex` — the image carries codex, not claude — answered every ask
recall-only while both seams were already routed through the declared provider; they now open
on any declared lane. The seam fix is code-derived: the empty container had no agents to think,
so it is gated by test, not by a measured ask.

**The Kubernetes manifest pins the image by digest.** Every release wrote a tag into the
manifest and nothing came back to finish it, while the manifest's own comment said a tag is a
promise and a digest is the artifact. Image promotion now appends the multi-arch index's digest
to the one `image:` line once that index has been composed from the verified per-architecture
manifests, and the next release's bump strips it with the tag, so a digest never rides under a
version it does not belong to. The manifest pins `0.9.42@sha256:b4ac2afc…` — the index the
published-image verification inspected. The promotion record is also now written after the
index is composed rather than before, so it says what was composed.

## 0.9.42 — 2026-09-02

**A mission's independent review now lands on the delivery, not on the doer's own narration, and
the critic lives long enough to give it.** Two defects, each measured on the second of two
missions on one topic. On 0.9.40 a doer that delivered at 341s found its critic gone: the reaper
measured a critic's idle time from the moment it started, and the room exemption that had kept
it alive lapsed the instant the commission completed — five seconds later it was stopped as
"idle 346s" with the review still to do. On the 0.9.41 build the doer's inner session, which
joins the room under the worker's `-mind` alias to coordinate mid-run, published its own
"complete" citing its own progress note 24 seconds before the harness's real completion; the
critic reviewed the first completion it saw, objected against a key no mission row carries, and
was reaped with the real review in flight. A critic now reviews a completion only when it comes
from a worker's own name (never its mind's) and names a commission that room requested or
accepted; a completion it declines is logged with the reason. A critic's idle clock starts at
its room's latest completion — the same 300 seconds the runtime already holds a completed
mission open for its verdict — so a long mission is reviewed and a settled room still releases
its critic. Both sequences are replayed in tests.

## 0.9.41 — 2026-09-02

**A team root now owns what the mesh learns.** With only `XMESH_TEAM_ROOT` set, the runtime
derived `<root>/sym` for its own reads, but the mesh library that writes every node store
resolves its root from `SYM_STATE_DIR` alone — so the server read canon from `<root>/sym/nodes`
while every operator and worker store was written to `~/.sym/nodes`. Measured on a fresh 0.9.40
install: the second mission on a topic was briefed with "Canon (0)" although the first had
completed and been objected to; in a container that home is off the volume, so a team-root
deployment also lost its stores when the container was replaced. The derived root is now
published into the process environment, where the library reads it and every spawned worker
inherits it; an explicit `SYM_STATE_DIR` is left exactly as set, and a deployment that names no
team root is not touched. Re-run on this build, the second mission's brief carried the first's
grounded objection as Canon and the doer built on it. The Docker `/state` path and the
Kubernetes manifest set `SYM_STATE_DIR` explicitly and were never affected. A test now asks the
library, in the same process, where it will write, and holds the import order that makes the
publication reach it.

## 0.9.40 — 2026-09-02

**Four security findings from the independent post-cut audit of 0.9.39, closed with refusals
proved live.** (1) The container entrypoint validated *after* mutating: a hostile
`XMESH_VOLUME_MOUNT=/` would have chowned the root filesystem, and an outside-volume team root
was created before the beneath-mount check ran — validation now precedes every filesystem
touch in every branch, with `/` refused by name and the refusal proven to leave nothing
behind. (2) The deployment-identity marker was write-then-rename, so two racing first boots
resolved to whichever rename landed last — it is now an exclusive create: one winner, one
comparer. (3) `XMESH_TRUST_PROXY=1` mapped to trust-all, which reads the *leftmost*
`X-Forwarded-For` — the entry the client wrote; the value is now a hop count and `1` trusts
exactly the one edge in front of the server. (4) The public-door credential posture checked
that a passcode was provided, not that it could survive a public door — an explicit credential
under `XMESH_REQUIRE_CREDENTIAL=1` must now be at least 16 characters — a floor that assumes a
generated secret (`openssl rand -base64 24`), which the refusal now says outright; the check
cannot distinguish a typed phrase of the same length. The audit's second pass added one more:
`XMESH_VOLUME_MOUNT` without `XMESH_TEAM_ROOT` crashed the root branch on an unbound variable
instead of refusing by name — it is a misconfiguration and is refused as one, before any
filesystem touch. Per the audit's own requirement these are cut as a traceable patch; no image
was ever rebuilt under the 0.9.39 label from post-tag source.

## 0.9.39 — 2026-09-01

**The image carries its mind (Architecture A, one image).** The distroless container answered
`ready:true` and died on `spawn codex ENOENT` at its first real mission (clean published-image
production gate): the `api:*` provider prefix names the auth source, not a different engine —
the engine is the headless CLI, and it was not in the image. The runtime base moves to
node:22-slim with the codex CLI pinned (`@openai/codex@0.152.0`, presence proved in-build), and
the image owns its startup path: verify the team root, supported stdin API-key login into an
ephemeral `CODEX_HOME`, verify, unset the key, exec the runtime — the key never appears on a
command line and never reaches the long-lived server process. No sshd ships: cloud access is
the platform's HTTPS proxy plus the app's own auth. The slim base arrived with 22
upstream-unfixed HIGH/CRITICAL OS findings; nineteen are removed outright (perl, the mount
stack, ncurses-bin, and the one CRITICAL — zlib1g, which nothing in the process tree links) and
three remain as time-bounded, owner-named exceptions because removing them removes the product
(gzip, libacl1 under coreutils, libtinfo6 under bash). Net: 0 CRITICAL, 3 ledgered HIGH, and a
shell — which `CHECK: run` grading needed all along.

**The login door is throttled, and cloud posture is explicit.** A user-self-deployed cloud pod
puts `POST /api/session` on a public HTTPS URL; the passcode's strength always assumed a
throttled attacker and there was no throttle. Per-address and global sliding-window fences now
answer before the credential check (429 with Retry-After; a success clears the person who
mistyped). `XMESH_REQUIRE_CREDENTIAL=1` refuses to boot when the passcode was not explicitly
provided — a generated secret printed to platform logs is not a posture anyone chose — and
`XMESH_TRUST_PROXY=1` opts the throttle into `X-Forwarded-For` behind a trusted proxy.

**The legacy spellings are retired.** `/api/commissions` and `/api/worlds` answer **410 Gone**
with the exact `/api/missions` spelling to use — the migration window the 0.9.37 rewrite
promised ("deleted in the next release") closes on schedule. The handlers survive unchanged as
the single internal implementation behind the rewrite. Internally the substrate now speaks the
surface's word too: `worlds.ts` is `missions.ts` (`Mission`, `MissionDeps`, `listMissions`,
`createOrJoinMission`, …); the durable store keeps its `worlds.json` filename so no existing
deployment's state is orphaned by a rename.

**Mission views say how the work ended.** `blocked` books by design (crew and slots freed, the
approval card is the human's to clear), so a mission whose every commission was booked read
`status: "complete"` even when nothing was delivered — a coarse poll took a run that died on a
spawn failure for a completion. `/api/missions` rows now carry `outcome` beside `criticStatus`:
`delivered` / `blocked` / `dismissed` / `mixed` / `pending`, derived live, with an unresolvable
commission reading `pending` rather than being dropped. Lifecycle `status` is untouched.

## 0.9.38 — 2026-09-01

**The grader honours both roots a hosted worker actually has.** A worker's own scratch narrowed
to `<root>/rooms/<room>` while mission criteria name absolute deliverables under the deployment
work root — the mind is granted that root explicitly (`--add-dir`) and writes succeed, but the
check grader only knew the scratch, so a completed mission persisted "path escapes the
workspace" 0/2 with a FAILED summary while the live `/detail` evaluation passed the same
artifact (codex-mac's clean-install production gate on published 0.9.37, mission-529cca — the
second one-artifact-two-verdicts hole in this grader, distinct from 0.9.37's spelling-alias
fix). The launch path's own comment already promised absolute criteria under the root "still
resolve"; the grader now keeps that promise: `runChecks`/`withinWorkspace` accept additional
sanctioned roots through the same shared containment lens — never alternative resolution bases,
so a relative target still resolves against the scratch only and its meaning is unchanged — and
the hosted worker passes its work root when the two differ. A sibling outside both roots is
still refused; API-visible paths still keep the caller's spelling. Regression pins the live
shape (`test/check-workroot-fence.test.ts`) alongside the 0.9.37 alias pins.

## 0.9.37 — 2026-09-01

**The deploy surface can pass its own gates.** A clean-operator audit of published 0.9.36
(codex-mac) found the Kubernetes probe exec'ing `node`, which does not exist on the distroless
image's PATH — the probe could never pass in-pod (`/nodejs/bin/node` is the binary, proved
against the published image). The manifest now explains team-root licensing where the operator
sets it — teamId binds to sha256(realpath(XMESH_TEAM_ROOT)), so no default path is ever
shipped: a baked-in root would give every cluster the same teamId and one licence would
validate everywhere. Its readiness comment now claims exactly what the probe gates (ready,
not mesh-grounded; R3 keeps this manifest pre-1.0 evaluation, API-only). The stale second
container build (deploy/Dockerfile — unpinned, root, unlicensable) is removed, with a test
holding it gone. And the transport boundary is explicit: `XMESH_BIND_HOST` reaches listen(),
and the README's "Production network boundary" section says the sentence an operator must
not miss.

**One artifact, one verdict — containment through one lens.** The check grader and /outputs
compared path SPELLINGS, and on macOS /tmp and /var are symlinks into /private: a worker graded
in its own cwd spelling while the daemon's filesystem-derived workspace was already resolved, so
a clean-install production gate saw persisted checks 0/2 and a FAILED summary over a file whose
live /detail evaluation passed, and /outputs called the same artifact unresolved (codex-mac,
mission ddced3). Containment now goes through workspace.ts's documented one lens
(realish/isInside) at every fence — the grader's three sites and /outputs — while API-visible
paths keep the caller's spelling and output identity dedups on the resolved form. The
commission's own room workspace is always in /outputs scope: by settle time its workers are
released, and a scope built only from live workers un-resolved artifacts sitting exactly where
the mission wrote them. Independently verified: codex-mac reproduced the full suite green and
re-ran the focused regressions against the fix commit.


**Dependency advisories scoped, not hidden (audit FIX 1/7).** `npm audit` on a plain install
resolves two HIGH advisories via `@huggingface/transformers` (adm-zip GHSA-xcpc-8h2w-3j85,
sharp GHSA-f88m-g3jw-g9cj). Every shipping mechanism was tested empirically — overrides,
npm-shrinkwrap in the tarball, global and dependency installs — and none changes what a
consumer resolves: the pins are upstream's. The repository tree now resolves patched versions
(overrides: adm-zip ^0.6.0; sharp resolves absent, and the text encoder passes without it,
which is the reachability proof), and the README scopes the posture instead of letting the
container sentence be over-read.

**README: the federation row stopped contradicting §4.** Line 59's parenthetical still taught
the pre-0.9.29 refuses-until-choice spend contract three sections above the §4 that correctly
describes the default cap of 40 — found by codex-mac's cross-surface audit. The CAT7 vocabulary needed no
change — README and site both already say "dimensions"; the audit claim of a mismatch was
checked against both fetched surfaces and refuted. The published 0.9.35
tarball's README carries the old parenthetical until the next release.

## 0.9.36 — 2026-08-31

**A critic is not staffing at the join door either.** The 2026-08-27 fix taught the supervisor
that a critic never claims a commission; the world-join filter was a second lambda under the same
name with the old any-live-worker semantics, so a mission whose doer was reaped but whose critic
survived stayed joinable — an ask pooled into exactly such a world, could never be claimed, and
paused at the bounded-liveness horizon (dev-team-2's audit e2e, mission-be2f89). One predicate now
answers both doors, a join candidate without execution capacity falls through to the 0→1 mint
path, and the regression pins BOTH call sites so a third door must choose the predicate or fail.

**Dependency advisories scoped, not hidden.** A plain npm install resolves two HIGH advisories via
`@huggingface/transformers` pins (adm-zip GHSA-xcpc-8h2w-3j85, sharp GHSA-f88m-g3jw-g9cj); the pins
are upstream's to move, and every consumer-side mechanism was tested empirically to change nothing.
The container image already mitigates its own copy — `adm-zip` is deleted after install, and `sharp`
stays as a tracked HIGH deliberately because the runtime's neural entry requires it eagerly (a
removal attempt broke the build; an overrides attempt broke the IMAGE build the same way and was
reverted — the in-image npm honors a tarball's overrides where host npm does not). The README now
scopes the npm-install posture beside the bounded container-scan sentence.

**Tracks `@sym-bot/sym` ^0.13.4.** Tonight's SDK fixes ride along: legacy inbox entries restore
fetchable instead of lost or unaddressable (0.13.3), and the replay-dedup TTL now exceeds the
daemon spool's ~30-hour replay horizon so a stale directive can no longer surface as fresh
(0.13.4). The pairing a user installs is the pairing that was tested.

**The federation row stopped contradicting §4 on spend**, and an "Operating discipline" section
states the reporting rules the audits converged on: instrument-and-scope in the claim's sentence,
a machine clock beside every reading, and the object named — a file is never a process.

## 0.9.35 — 2026-08-31

**Mission confirms reach the seat that asked, on every board.** A board started under a team root
read its loopback registry from `<team>/sym/loopback` — a directory that existed on no team — while
seats register host-wide in `~/.sym/loopback`. So the confirm projector never found a commissioner
live anywhere, fell back to the mission room, and emitted from an operator the seat could not hear:
"record stored, wire missed". Three of four boards on the reference host only worked because their
launchd plists had been hand-corrected with `SYM_LOOPBACK_ROOT`; the newest board ran the default and
delivered nothing (dev-team-2, mission-be2f89). The registry read now includes the host root by
default (`SYM_HOST_DIR` overrides it so an isolated run stays isolated). Verified on the live
registry under that board's environment: `sym-bot-room` absent before, eight live members including
the seat after.

**`xmesh mcp install --seat` repairs a bridge that already exists.** A seat given on the command is an
instruction: it is applied to an existing `.mcp.json` entry that has no seat or carries a different
one, and the previous value is reported. A seat that is merely in the environment never rewrites a
file. Found because one seat's hand-wired bridge existed with no seat at all and the install step
treated "present" as "right" — every mission it had ever filed was anonymous with confirms off — and
another seat's name lacked the host suffix its node carries, which the projector matches exactly.

## 0.9.34 — 2026-08-31

**The MCP bridge is part of installing xmesh, not a step after it.** `xmesh-mcp` shipped inside
the package from the start; wiring it was README §3, a JSON block to copy by hand. Two of five
seats never did, because nothing in the product said there was anything to do — and one of
them spent twenty minutes driving the raw HTTP API by hand to file a defect the bridge does in
one call. Now every `xmesh` command run from a project directory wires the bridge into
`./.mcp.json` if it is missing, repairs it if it names an install that no longer exists, and
leaves it alone if it is right; `xmesh mcp install --url … --seat …` is the explicit one-line
form. The bridge path is the one shipped with the install running the command, so "which
xmesh does this bridge follow" is answered by construction. A launchd daemon at cwd `/` is
refused — it cannot know where a seat lives and must not guess. A seat is written only when
given, never invented from a folder name: the name must be a node the mesh can reach.

**`/api/worlds` and `/api/commissions` still answer — one release later than 0.9.33 said.** The
0.9.33 notes said they would be deleted in the next release. They are not, and the reason is
stated rather than left to be discovered: `symday-ios` pins `sym-mesh-engine` 0.6.15, which
calls the old paths, and moving it crosses two engine minors with breaking changes between
them. That migration is its own piece of work and had not happened by this release. The
window closes in the release after `symday-ios` is on engine 0.7.2 or later; the `/api` index
still lists the old spellings under `legacy` and says they are leaving.

## 0.9.33 — 2026-08-31

**One word on the HTTP surface: `/api/missions`, and one creation door.** The API had three words
for one workflow — `world` was the door, `commission` was the ticket, and `mission`, the word the
MCP tool, the CLI verb, the room names and this changelog all already used, appeared on neither.
An experienced operator asked to file one defect on their own board searched HTTP for "mission",
found nothing, then found two creation doors with nothing to tell them apart and picked the wrong
one from a comment in a test file. `POST /api/missions` with a `room` files into that room;
without one, a mission room forms and staffs itself. `GET /api/missions` returns both lists — that
merge used to live inside the MCP bridge, so every other client had to know to do the same or show
half the answer. It is a rewrite onto the existing handlers, not a second implementation: twenty
duplicated routes are twenty that drift.

**`room` on the new door is the destination.** On `/api/worlds` it meant the *origin* an ask came
from, while the recipe routes read the same name as the destination work runs in. Any caller that
passed `room` meaning origin now sends `originRoom`. Three did — the CLI's `offer`, and the
station's offer and lint — and left unchanged each would have quietly become "file a ticket into
that room" instead of "open a mission from it".

**`/api/worlds` and `/api/commissions` answer for this release only.** They are deleted in the
next one. XMesh is pre-1.0 and in evaluation, so this is a migration window for first-party
clients, not a compatibility promise. The `/api` index lists them separately and says so.

**A route index, a JSON 404, and a board that will say what it is running.** `GET /` named two
routes out of the whole surface and there was no index, so a guess at a path returned an Express
HTML error page on a board where everything else is JSON. `GET /api` now lists every route,
unauthenticated — the caller asking what exists has no credential yet, and every path is already
in this package's public README. An unmatched path answers JSON naming the nearest real route.
And `/api/version` now answers a loopback caller without a credential: it was behind auth on every
port, so two seats independently could not ask a board they were standing on what version it was
running. Loopback concedes nothing — that caller can read the `package.json` the endpoint reports
from — and it is judged on the socket address, never a header.

## 0.9.32 — 2026-08-31

**Erratum: 0.9.31's notes claimed a release "can no longer half-ship", and 0.9.31 half-shipped.**
That heading was written on the strength of the credential preflight, which is real but narrower
than the sentence: authentication is proven before any irreversible step. The same release
disproved the broader claim. 0.9.31 reached npm while the container registry still served 0.9.30
as `latest`, and `deploy/kubernetes/xmesh.yaml` — rewritten, committed and pushed by the release
itself — pinned a `0.9.31` image that did not exist. Anyone deploying from the manifest in that
window would have got nothing. The images were promoted by hand about twenty-five minutes later
and verified layer-for-layer against the artifacts the gate scanned. The heading is corrected
here and an erratum is on the README, which is the page a reader lands on; the tarball published
to npm carries the original wording and cannot be amended in place.


**A release cannot report itself aligned while its image is missing.** The `deploy refs` gate
checks the manifest pin matches the version being released; it cannot check the pin is real. The
release now inspects the registry for the image its own manifest pins, and where the image is
absent it names the release incomplete, prints the command that finishes it, and exits non-zero.
It does not revert: a publish cannot be undone, so the release refuses to *call* itself aligned
rather than pretending it can roll back.

**A release notices when it produced less evidence than the one before it.** Nothing failed when
0.9.31 half-shipped — the fault surfaced only because someone diffed the evidence directory
against its predecessor's and saw two files missing. That was a habit, not a control, and its
failing input was "the directories match", which is the default. The diff is now a gate, and it
is deliberately ignorant of what any file means: if a kind of evidence the last release produced
is absent, something that used to happen did not happen. It generalises past image promotion to
whatever the next separate deliberate act turns out to be.

**The release bumps pins, not prose.** The version bump replaced every occurrence of the previous
version in the README and the deploy manifest, so a sentence naming a version — a claim about
history — was rewritten along with the install commands. "Recall expands the cluster, not just
the nearest row" shipped in 0.9.22 and was dragged forward through nine consecutive releases, so
the front page announced that feature as new in each of 0.9.23 through 0.9.31. It is corrected to
0.9.22. Only genuine pins are rewritten now, an enumerated pin form left unbumped fails the
release rather than shipping, and the claims gate checks every feature attribution on the front
page against that release's changelog entry.

## 0.9.31 — 2026-08-31

**`xmesh review --seat <name>`.** The seat decides who a completion is addressed to.
`XMESH_SEAT` has always worked; the flag exists because who is asking changes between
commands in a way the deployment's passcode does not. Without a seat the commissioner is
recorded anonymously and no confirm is ever sent — deliberately, since a message addressed
to a name identifying the deployment rather than the asker is worse than silence — and
`xmesh review` now tells you that at the moment you commission the work.

**Routing is decided where commissions are created, not remembered at each door.** Two
creation paths never recorded a return route, so work booked through them could not report
accepted, blocked or completed to anyone, at any seat. The route is now a required field on
the one creation path they all share: a new door cannot be added without deciding who hears
about the work, and "nobody" is a stated answer rather than a default.

**The README is now gated like the artifact.** Every factual claim on it is checked against
a source of truth in the repo — the release date against the CHANGELOG, every documented
endpoint against the routes the server registers, every `xmesh` verb against the CLI, the
documented port against the runtime default, every pinned version against the package, and
every advertised flag against what the CLI reads. It caught its first defect immediately: the
front page had dated this release six days early.

**The publishing credential is verified before anything irreversible happens.** 0.9.30 bumped, committed, tagged and pushed before failing to publish,
leaving a tag pointing at code that was not on the registry. Authentication is the only step
that depends on something outside the repository, so it is now the first thing checked.

## 0.9.30 — 2026-08-30

**A commission created through the direct door can now tell you it finished.** `POST
/api/commissions` recorded no return route at all, so the ticket was refused by the
send-back for having nowhere to report to — it could never announce accepted, blocked or
completed, at any seat, however it was called. `/api/worlds` and both recipes already
routed; this door and `/api/teams` did not. `/api/teams` still does not, and is named in
the test that tracks the gap rather than hidden behind a threshold.

**`xmesh review` says when nobody will be told.** The API has always returned `notifies`
on that response and the CLI never printed it, so a review could be commissioned, run for
minutes, write an artifact and finish in silence, with nothing anywhere explaining why.
It now names the anonymous commissioner, says why an anonymous confirm is suppressed
rather than merely suppressing it, and gives the two ways to fix it.

**`xmesh review --seat <name>`.** The seat decides who a completion is addressed to, and
until now it could only come from the environment. `XMESH_SEAT` has always worked and
still does; the flag exists because who is asking changes between commands in a way the
deployment's passcode does not. The name must match a node the mesh can reach — a confirm
addressed to an unreachable name lands in the operator's store and nowhere else.

### Note

If a completed mission has ever finished without telling you, set a seat and it will. A
commissioner recorded as `anon@<user>` is deliberately never confirmed: a message
addressed to a name identifying the deployment rather than the asker is worse than
silence. That behaviour is unchanged — what changed is that you are now told it is
happening, at the moment you commission the work.

## 0.9.29 — 2026-08-29

**A fresh deployment now runs, instead of refusing until someone picks a number they
cannot yet know.** The spend contract required a one-time human choice before any paid
work; a new operator met that gate on their first commission, with no idea what a work
unit costs or what their daily volume would be. Of the three modes, two demand a number
they do not have.

A fresh deployment now runs under a default of `workload` / 40 daily work units — the same
figure the refusal has always advertised as measured-normal, against real use of about six
missions a day. When that default binds, the refusal says plainly that nobody chose it and
reports what a day of your own work actually cost, which is the fact you lacked at first
contact. The decision moved to the moment you can inform it.

`unbounded` is still never a default. Accepting unlimited provider spend takes an explicit
call with a name recorded against it, and `/api/health` now carries `chosen`, so a cap
nobody picked stays distinguishable from one someone did.

**Two refusals that named a rule and withheld the way to satisfy it.** A wrong-protocol
`401` and the spend refusal both stated policy without stating the remedy. The passcode is
not a bearer token — it is exchanged at `POST /api/session` for a session token — and
nothing in the `401` said so, so an operator with the correct secret could not tell a
protocol mistake from a bad one. The spend refusal named no route at all, and there is no
CLI verb or cockpit control for it. Both now carry the exact call.

### Fixed

- `reconcile-verdict-weights` reported only the rows it could compare and printed that as
  the population. It now states the whole ruled set and names each excluded class — rows
  with no completion key can never carry a weight, and that is an answer, not silence.

## 0.9.28 — 2026-08-29

**A dismissed verdict no longer reports success it cannot verify.** `dismissCMB` returns
nothing — there is no result to read — so the dismiss branch returned `committed: true`
unconditionally, whether or not the store moved. That is the same assume-success shape as
the defect 0.9.27 fixed for the validated half, left standing in the half that had never
once fired in production (163 dismissals, zero weight writes on one deployment).

Both halves now read the entry back off the store and compare `anchorWeight` against what
is being claimed. A write that did not land reports `write-not-observed` instead of
success, and `POST /api/lifecycle/:key` returns that reason rather than `committed: true`.

This affects anyone on 0.9.27: a dismissal there reports a committed weight of 0.5 with no
check that the store took it. The validated half of 0.9.27 is unaffected and was confirmed
against the published artifact on a second deployment, on a block held by four stores where
only the author's moved.

## 0.9.27 — 2026-08-29

**An operator's verdict now reaches the memory it is a verdict about.** Ruling on a
commission was supposed to move the authoring agent's block: validated to weight 2.0,
dismissed to 0.5. In practice the commit gave up whenever the author was no longer
resident — and crews dissolve by design, so by the time a person rules, the author is
usually a dead name. Measured on one deployment before the fix: 61 validations produced
26 weight writes, and 163 dismissals produced **zero**. The negative half of the rule had
never once fired, so `anchorWeight` could only ratchet up and recall had no way to tell
dismissed work from validated work.

The author's process ends; its store does not. Verdicts are now committed against the
store on disk, and the result is read rather than assumed — a store that does not hold
the block reports `not-held` instead of logging success over a no-op.

**Verdicts credit the agent that authored the deliverable, not one that holds a copy.**
Authorship came from a store lookup, which returns whichever store happens to hold the
block — and a reviewed deliverable is held by at least two, the doer that wrote it and
the critic that remixed it. Validating a reviewed mission could move the critic's weight
and leave the author's untouched. The commission row names who delivered; that is now
what decides, with the lookup only as a fallback.

**`POST /api/lifecycle/:key` reports whether the weight moved.** The response carries a
`weight` object (`committed`, `agent`, `value`, or a `reason` when it did not) alongside
the recorded ruling. `ok: true` previously said only that the ruling was filed, which is
a different thing from the store write that goes with it.

**Tools read the corpus their board actually uses.** Scripts resolved the node tree with
their own copy of the state-root rule and dropped a branch of it, so with no explicit
`SYM_STATE_DIR` they read `~/.sym` instead of `<team root>/sym`. That default is right
for some deployments and wrong for others, and the failure was silent: a tool would
describe a corpus it had never opened, and in one case reported a repair as needed
against blocks that were fine. Resolution now goes through the server's own resolver in
`reconcile-verdict-weights`, `stranded-verdicts` and `recall-trials`; each run prints the
corpus it opened; and a run with work to do and no stores to do it against refuses with
exit 3 rather than reporting nothing to do.

**Discovery respects tenancy.** The registry behind it did not, so every board was
joining every tenant's rooms. Cross-team joining itself stays open by design: agents join
a team by capability and admission, and filtering rooms by tenant suffix would have closed
the mesh rather than fenced it.

### Added

- `scripts/reconcile-verdict-weights.mjs` — brings block weights into line with the
  verdicts already standing on their rows. Report-only unless `--apply`; writes only where
  a store carries no ruling of its own, and reports disagreements rather than settling
  them.
- `scripts/verify-verdict-routing.mjs` — stands up a throwaway deployment and asserts that
  a verdict reaches a dissolved author's store, reading the weight back from disk rather
  than trusting a log line. Reports "cannot test" (exit 2) distinctly from failure when the
  environment cannot produce an authored block.

### Changed

- Minimum `@sym-bot/sym` raised to `^0.13.2`.

## 0.9.26 — 2026-08-27

**Two ways to spend without being counted, both closed.** A commission row could be created
without ever reserving a work unit — six of seven places that created one did exactly that,
including the endpoint that spawns a crew. And a mission whose *originating room* was
misspelled slipped past the daily budget entirely, because an unknown room has no budget and
"no budget" was read as "not over budget". Creating a commission now requires proof it is
allowed to exist, and the proof cannot be forged: the compiler refuses any new creation site
until whoever adds it says which it is — reserved, or deliberately exempt with a written
reason. The six existing exemptions now carry theirs in the code, where a reader sees them.

**Your fleet no longer refuses work while idle, and no longer loses it on restart.** Run slots
were held by worker *processes*, so workers waiting with nothing to do consumed the capacity
that new work needed; capacity is now measured by the work actually in flight. Separately,
restarting the runtime killed in-flight workers by design but left their missions claimed by a
worker that no longer existed, so they were parked minutes later as "abandoned — worker gone",
which read as a crash and waited for a human who was never told. A restart now hands that work
back and it is picked up again automatically. And a room staffed only by a *critic* no longer
counts as busy — a critic never claims work, so missions there waited fifteen minutes for a
claim that could not arrive.

**Rooms are separated on disk, not only on the network.** Each room's workers now get their own
working directory. Previously every worker on a deployment shared one, so two rooms — including
two different tenants — could write over each other's files while being correctly separated
everywhere else. Deliverables are unaffected: they still land where your criteria say.

**A room name is checked wherever you can send one.** Seven request paths accepted a room
without asking whether it was one, so a typo could place agents in a shared room, run a mission
on the wrong models, or bypass a budget. All of them now answer 400 and say what was wrong.
`POST /api/worlds` additionally accepts `originRoom`, which is what that field has always meant
— the room an ask comes FROM, not where the work runs; `room` continues to work unchanged.

**A worker can tell whether its model is running, waiting, or gone.** It used to report "still
working" whenever output went quiet, which is the one thing silence cannot establish — a quiet
model may be thinking, wedged, or already dead. Progress notes now say which, and a model that
exits without finishing fails immediately instead of holding the run for another quarter hour.

**Upgrading.** Requires `@sym-bot/sym` 0.13.1 or later. On first start after upgrading you may
see log lines about crews moving to their room's own directory; that is the migration above and
needs nothing from you.


## 0.9.25 — 2026-08-27

**Your fleet was refusing work while sitting idle.** A run slot was held by a worker
PROCESS — taken when it started, released when it exited — but the thing a slot protects
is a model actually running. A worker waiting in a room for something to do is idle and
costs nothing, and yet it held a slot for as long as it lived. On one deployment four
workers alive for a day, with seven minutes of CPU between them, held four of six slots
while missions queued behind "at capacity" and never started. Nothing had crashed or
leaked; the wrong thing was being counted. Capacity is now measured by the work actually
in flight, so idle capacity is available capacity, and a fleet that looks busy has to be
busy. Workers are still bounded — by their own limit, `XMESH_MAX_RESIDENTS`, because a
worker costs a process even when it costs no model.

**A mission that could not be staffed used to disappear.** It was torn down and marked
finished-without-starting, carrying a reason that promised it would resume when a slot
freed — and nothing ever resumed it. It now retries, and if it still cannot be staffed
after several attempts it stops and waits for you, with its reason intact. The promise
this runtime makes is that a mission completes or comes back to a human; that case did
neither.

**The retired shared review room came back on every restart.** 0.9.23 scoped review rooms
to your team, but crews saved before that release were restored exactly as written, so
each restart put a crew back into the old shared room — where another team's crew could
see it. Saved crews now follow the current scoping when they are restored, and one whose
scoped replacement already exists is dropped rather than duplicated. Mission crews are
untouched: their rooms are unique already. Every migration is logged. If you have run a
team deployment since 0.9.23, expect a line or two about rooms moving on your next start.

**Room names are refused, never guessed.** A name that is not a valid room used to be
quietly rewritten into one that was, and a missing name was quietly replaced with a
default — so a typo, or an empty field, could land agents in a room nobody chose, or
strand them alone in one nobody else can find. On a local network the room name IS the
boundary, so both behaviours are now errors you can see instead of silence you cannot.
Requests that named no room, or an unusable one, return 400 and say why.

**Also in this release.** Requires `@sym-bot/sym` 0.13.1 or later, which fixes clients
that connect without naming a room being refused by any named room. An internal node whose
name suggests a team but whose room does not now says so in the log, because the name was
being read as proof of isolation that only the room provides.


## 0.9.24 — 2026-08-26

**Fixes review missions on team deployments (0.9.23 regression).** On a production team
root, 0.9.23's scoped rooms made the room's internal operator name carry the team suffix
twice, exceeding the mesh's 64-byte identity limit — every review-recipe mission failed to
start. Fixed, and hardened: operator names are now bounded for any room name length (long
engagement names were over the limit even before the regression), kept verbatim whenever
they already fit so existing deployments keep their identities.

## 0.9.23 — 2026-08-26

**Rooms are yours by default; deliverables always come home.** Two halves of one boundary
ruling, built from a week of real incidents where one tenant's review requests were worked by
another tenant's crew and the finished files landed in the wrong tree.

**Scoped rooms (the C half).** Recipe rooms — the review desk, engagement rooms — are now
scoped to your team automatically. A bare room name takes your team's suffix; a name that
already carries a team suffix is kept exactly as written, which is how you deliberately share
a room with another team. Installs without a team root are unchanged. One consequence to
know: rows commissioned under the old shared name before this release keep re-offering into
that shared room until they drain.

**Delivery by your own runtime (the D half).** A volunteer from any team may work your ask;
the deliverable body now returns over the mesh with the completion, and YOUR runtime writes
it into YOUR tree — never the volunteer's sandbox writing into yours. The write path is
deliberately narrow, and each limit is enforced, not aspirational: the target path comes only
from the mission's own criteria (never from the network); it must live inside your workspace;
only the worker that accepted the mission can have its bytes land; nothing existing is ever
overwritten (the write is create-only at the filesystem level, so even a planted symlink at
the target is refused); and the body must match the hash its signed completion declared.
A completion whose deliverable neither arrived on disk nor over the mesh is a visible
"pending at home" state on the mission — never silence. Mesh-carried bodies are capped at
256KB of UTF-8 text; larger or binary artifacts still travel as locators.

This delivery path was adversarially reviewed before release; the review refused the first
implementation outright and every one of its ten findings is fixed and pinned by a test in
this release.

**Docs.** `jq` is now listed in Requirements (the quick start uses it); the session endpoint
documents that both `credential` and `passcode` work as the field name; the recall
anchor+expand join shipped in 0.9.22 is now documented, including `expand=1` on `/api/query`.

## 0.9.22 — 2026-08-26

**Recall now finds the whole cluster, not just the nearest member.** The mission brief's
recall section gains anchor+expand: the top-ranked memories that belong to this deployment's
own mission rooms each pull a bounded set of same-room siblings into the brief. The mechanism
exists because the measurements demanded it — four independent attempts to improve the
encoder or the text it embeds all failed statistical gates, while the encoder was found to
reach the right mission room about eight times more often than the right memory. Structure
closes that gap: measured at byte parity against plain retrieval (same brief size), expansion
roughly triples how often the memory an agent needs is actually in its brief, and the live
deployment reproduced the effect on its acceptance run.

Expanded rows are honest about what they are: they render `[via <anchor>… room]` instead of a
relevance score they never earned, they never cross a tenant boundary (only rooms this
deployment's own registry commissioned can join), the calling seat's own cognition is never
expanded back at it, and sibling text is capped at three times the anchor text so ranked
memories always anchor the brief. The query surface exposes the same join behind `expand=1`
on `/api/query` for anyone who wants to measure it.

**A stranded permission stop can now be acknowledged.** A mission blocked on a capability
whose crew is gone and whose deliverable was hand-carried to its destination was closable by
no verb; the operator's verdict now closes it — behind an explicit flag, so a live permission
wall can never be ruled away by accident.

**The paraphrase result, for the record.** This release's measurement campaign also settled
the founding question: on a blind, independently-generated paraphrase benchmark, memories are
found by meaning at essentially rank one (53/53 at top-10 against fourteen and a half
thousand distractors). Meaning-reachability was never the gap; the sibling gap above was —
and this release closes it.

## 0.9.21 — 2026-08-25

**Review crews no longer litter the reviewed repo.** Reviewing a file staffed workers whose
working directory was the file's own folder — so worker evidence and scratch landed as
untracked strays inside whatever repository the target lived in, and the crew's real
deliverable directory was outside its writable scope (every such review ended in a permission
stop). Crews now live in the deployment's own workspace: evidence, scratch, and the review
itself land under your team's `work/` directory, and the target is read where it is.

**A stale MCP bridge announces itself.** Bridge code loads when your client connects, so a
daemon upgrade under a live session leaves the session reading old surfaces — a tenant
measured 238 cards as missing provenance that was present in the store the whole time. Every
tool answer now carries a version-skew banner naming both versions and the remedy until the
client reconnects.

**A stop with findings can be ruled.** A mission that stopped honestly on disclosed findings
and lost its crew was closable by no verb — dismiss said "rule on it", ruling said "no
completion to rule on". Blocked commissions with disclosed findings now take the operator's
verdict directly; a bare permission stop still resumes or abandons.

**Budget management has one clear page.** The README now documents the whole surface in one
place: the required one-time spend choice (all three modes, the endpoint, where your state
files live), optional per-room daily USD lines with the recorded validator override, and the
cost levers that are not budgets — with the pricing separation stated where confusion would
start: XMesh never meters you for money.

**Sharper mission authoring.** Ask-lint warns when a CHECK path sits outside the worker's
writable scope (an unsatisfiable-as-dispatched mission, found by a mission it happened to).
The release pipeline's post-publish verification now retries the registry's read replicas
before reporting failure — it false-failed two healthy releases in a row.

## 0.9.20 — 2026-08-25

**The mesh reviewed its own release, and this version is the fixes.** With the external
reviewer out of commission, 0.9.19's spend-admission core was put through XMesh's own
adversarial review plus two live verification missions. They came back with fifteen code
findings and seven first-user findings. All are fixed here; the worst ones first:

**A restart can no longer eat fresh finding cards.** The board migration that supersedes old
flood cards believed it was one-shot but ran at every boot — and its "untouched" test exactly
matches a brand-new card nobody has triaged yet, so a card minted on one boot was silently
dropped on the next, unrecoverable. The migration now runs once ever (a persisted marker), runs
before any live disclosure can race it, counts what it actually moved, and every superseded
record now names its successor card instead of a boilerplate sentence — including the ones
written before this fix.

**Two identical asks at the same instant are one mission.** Concurrent same-text asks could
both pass the duplicate check and both announce work to the room. Admission is now
singleflight: the first caller runs it, every simultaneous duplicate receives the same
answer — one announcement, one mission. The duplicate check also normalizes whitespace and
case, and the rare post-announcement duplicate is charged rather than refunded, so refusals
can never be farmed into free announcements.

**Refusals keep their full explanation everywhere.** A spend refusal at a Board card start now
carries the original verdict's wording and options (it was being rebuilt from a later state
read that lost both), and the two assessment/review recipes no longer answer with a bare 503 —
they render the same structured refusal as every other door, and they stop the workers they
staffed instead of leaving them billed as standing capacity.

**Board records survive restarts whole.** A card's origin (which mission disclosed it) and its
started-mission anchor now reach disk the moment they exist — a crash could previously lose
the anchor and let a retry start a second mission. A correction records the old detail as well
as the old title, and an agent's correction can no longer silently re-impose itself over a
human's later hand-edit at every restart.

**The first-user story is truthful on every door.** The five-minute start gains the one-time
spend choice as its own step, stated before the refusal happens, with the exact call
(0.9.19's published README walked a fresh user into an unexplained 402). The HTTP quick start
says the same. The health example shows the `spend.protection` key that explains the state,
and the licence 402 and the spend 402 are disambiguated where a searcher lands. The provider
probe reports installed-vs-authenticated honestly, and a gateway base URL ending in `/v1` no
longer doubles into `/v1/v1`.

**Mission authors get warned about unsatisfiable briefs.** Ask-lint now flags a CHECK path
outside the worker's writable scope — found by a verification mission that hit exactly that,
in a brief this project wrote.

## 0.9.19 — 2026-08-25

**A spend refusal now means nothing happened.** The daily spend contract reserved its unit
after the mission request had already been announced to the room — so a refused mission could
still be picked up and worked by a standing agent. The order is now: duplicate check first,
reserve second, announce last. A refused ask leaves zero trace a worker could act on, two
simultaneous asks for the last unit admit exactly one (the other gets the structured refusal
with the one-time choice options), an ask that fails after reserving returns its unit to the
day (ledgered), and asking for something already open costs nothing and returns the existing
mission. Starting a mission from a Board card is protected the same way: two clicks are one
mission, and a spend refusal there answers with the same structured 402 as every other door.

**Findings appear on the Board the moment they are disclosed.** Previously a delivered
finding only became a card at the next restart, because the projection listened for a status
change and a disclosure doesn't change status. Cards now mint before the disclosing call
returns, and a projection failure is a visible counter on `/api/readiness`, never a silent
stall.

**A mission that was refused after "queued" can tell you so.** When capacity forced a 202
ticket and the spend contract then refused the work, the ticket's mission id resolved to
nothing. Every refused ticket now persists with the full reason, so `mesh_missions <id>`
answers "never started, and here is why" instead of silence.

**The Board never deletes, and it can be corrected.** The one migration that physically
removed flood cards is now an on-the-record supersede: cards move to `dropped` through the
legal table with a note saying why, hidden from the default view (`?include=all` serves
everything, and the response says how many were hidden). Stale wording is corrected in place —
by an authorized Board call or a `corrects: <card-key>` field on a new disclosure — with the
original kept in the card's history and never a duplicate row. Findings carry their whole
field set now: `next:` (the action) parses alongside closes-when, the owner is the card's
owner field, and the per-field cap that was truncating exactly the actionable parts is 1600.

**The Station board acts.** A validator can move Proposed cards to Ready and start Ready
cards from the card itself; a participant sees the record plus one honest line about what
acting requires. Spend refusals render on the card — state, reason, options, endpoint — never
in a browser alert. A settled mission no longer offers ruling buttons its own summary says
aren't needed, report filenames no longer leak worker identities onto the default screen, and
in-flight tickets show an evidence rail (sources inspected · checks · deliverable) with
blockers interrupting the card with the exact action needed.

**Sharper edges, honestly labeled.** Room preserve/unretire and worker preserve now require
validator authority. A gateway base URL ending in `/v1` no longer produces `/v1/v1` requests,
and the CLI provider probe says plainly that a passing `--version` proves installation, not
authentication. The ask console derives the intent from your task's own leading verb instead
of warning about a hidden Advanced field. `package.json` and the README now say where to
report defects (contact@sym.bot). The Kubernetes sample calls itself what it is — a pre-1.0
evaluation sample, API-only — and the licensing page carries a dated decision record for the
pre-1.0 boundary plus how to start a paid pilot.

## 0.9.18 — 2026-08-24

**An anonymous ask is told so, at the ask.** 0.9.17 deliberately sends nothing to a mission
whose requester has no identity — but the requester only discovered that by reading the docs.
`mesh_mission` now says it in the reply, in plain words: completion notifications are off, why,
how to restore them (set `XMESH_SEAT`, restart the MCP connection), and how to follow this
mission meanwhile. The MCP bridge also warns once at startup when it has no seat, and the quick
start ships `XMESH_SEAT` with two sentences on what its absence costs.

**A mission can announce its waiting-states to a room — opt-in, and it says almost nothing.**
`mesh_mission` gains `room` and `notifyRoom`. Off by default. When opted in at creation, the
states a team waits on — blocked, paused, completed, validated, dismissed — are announced into
the originating room as **the status and the mission id and nothing else**: no artifact or
evidence paths, no reasons, no ask text. A blocked reason can quote a customer's systems; the
mission id is the pull handle, and anyone authorized pulls the record through their own access.
Staffing events never broadcast, even opted in.

**The recovery claims are now proven, not promised.** Two tests run in fresh processes against
real registry files: a brand-new session reads a finished mission's full record — every
transition, the route, the artifact — after its world is gone; and a moved state root resumes
confirm delivery exactly where the cursor stopped, with nothing replayed from zero and nothing
skipped.

## 0.9.17 — 2026-08-24

**A mission now tells whoever asked for it.** Until this release, `mesh_mission` returned an id
and the sentence "watch it with mesh_missions" — and that was the whole contract: a mission
completed or paused *if you polled*. The mission's own room dissolves at teardown, so completion
was announced to an operator node that was about to cease to exist, and the person who
commissioned the work was never told. Observed twice on one day, including on the mission that
was reviewing the fix.

- **The ticket knows who asked.** Every ask records a return route on its own commission —
  immutable once set, one per asker (a joined mission carries the joiner's route too, so a
  second commissioner is never silently dropped). The seat identity survives daemon restarts;
  `mesh_mission` accepts an explicit `replyTo`; an ask with no identity is recorded anonymous
  and — deliberately — nothing is sent to it, because a confirm addressed to a name that
  identifies the deployment rather than the asker is worse than silence.
- **Booked trades confirm back.** Accepted, blocked and paused (with the reason), completed
  (with the artifact and evidence paths), validated and dismissed each project one targeted CMB
  to the commissioner, delivered where they actually are — the runtime resolves the requester's
  current room from the live registry, waits a bounded moment for the peer handshake, and
  addresses the wire frame by peer id. Verified end to end on a live deployment: both confirms
  arrived at the requesting session, unpolled, and were admitted into its store.
- **Delivery is not the record.** Every transition lives in the commission registry regardless
  of any wire outcome; the projection cursor advances on emission, receivers dedupe on
  `(key, seq)`, and a requester who was offline reads the full history — `mesh_missions` now
  serves every transition a mission passed through, with rows synthesised from pre-history
  registries explicitly marked, instead of the current status alone.
- **Under load the caller still holds a key.** A queued (202) response used to carry no mission
  id at all — the id did not exist yet. It now carries a preminted key that resolves whether
  the ask mints a new mission or joins a running one.

What this release does NOT do, stated so nobody reads more into it: confirms are push
best-effort — the registry is the source of truth and the pull surface is the recourse; nothing
is ever broadcast into a room (room-wide announcements remain unbuilt pending a privacy ruling);
and anonymous asks receive nothing, on purpose.

## 0.9.16 — 2026-08-24

**A node's local state now moves when it admits a peer's cognition.** This picks up
`@sym-bot/sym` 0.12.2, which fixes a defect in the substrate: every local-state update was reached
from init, broadcast, or the node's own `remember` — nothing on admit. The neural gate re-encoded
state after storing an admitted block; the heuristic gate, which is the production default, did
not. So in practice a node that had admitted five hundred peer blocks carried exactly the same
local state as one that had admitted none, even though the context it encodes is built from the
store those blocks land in.

**This changes no behaviour you can observe, and is released for that reason rather than in spite
of it.** SVAF scores per-category CMB vectors against anchor memory and does not read the hidden
state, so no admission decision changes. The re-encode is also still stateless — prior state is
discarded and recomputed from the store — so nothing here is recurrent and nothing learns. What
the fix buys is that local state is real, which is a precondition for anything built on it.

An unreleased fix in a dependency reaches nobody: XMesh resolves `@sym-bot/sym` from the registry,
so the substrate commit was inert until it was published and picked up here.

## 0.9.15 — 2026-08-24

**The mesh could not recall most of its own cognition.** Recall searched at most twelve *live*
nodes. There are 529 stores holding cognition on a working host, and none of the 173 mission rooms
qualified — a mission room stops being live the moment the mission ends, which is exactly when what
it learned becomes worth keeping. Measured on a real deployment: **727 of 1,121 findings produced
inside missions, 64.9%, could not be retrieved by any query.** Not ranked low. Absent. An exact
substring search missed three of four sampled findings entirely.

- **The cause was a borrowed question.** `discoverActiveNodes()` answers *who can I talk to* —
  liveness-gated, capped at twelve. Recall and the pre-work brief used it to answer *what do we
  already know*. Those are different questions, and only one of them is about the present moment.
- **Recall now searches memory.** Every store that holds cognition, live or not. Presence surfaces
  — the roster, the gateway, issue routing — still use liveness, which was always right for them.
- **It got faster, not slower.** CMB files are content-addressed and immutable, so each is read
  once: **22,154 entries in 82 ms**, against 184 ms for the previous 3,218. The index keeps only
  what ranking reads (74 MB), not whole records (325 MB).
- **A side effect worth naming.** Anchor ownership — who owned the block a piece of cognition was
  measured against — resolved for 67.6% of anchors before, because an anchor usually points into
  another node's store and twelve stores could not resolve it. It now resolves **98.5%**, over six
  times as many anchors.

**Progress chatter is no longer treated as memory.** Delivery receipts (`verified receipt of
cmb-…`) and activity notes (`using Grep`, `reading canon-context.md`) are emitted for the live
mission view and were being ranked as though they were findings. They are excluded from recall by
their declared CAT7 `intent`, and the count excluded is reported alongside every result — a
population that silently drops entries is the defect this release exists to fix.

**The pre-work brief is bounded, and says so.** Widening the corpus 4.4× while the brief still took
a fixed top twelve would have made it quietly more selective. It now draws a candidate pool
proportional to the corpus and packs to an explicit byte budget (`XMESH_RECALL_BYTES`, default
6,000) in score order, after collapsing repeats of the same cognition by text *and* by shared
lineage — a block admitted by five peers is one thought, not five. The brief prints its own
arithmetic: candidates considered, rows packed, bytes against budget, and what the budget dropped.

**Three surfaces were reporting confidently wrong things.**

- `GET /api/readiness` now reports semantic recall's real state, plus the size of the memory it
  searches and the budgets it packs to. The previous readiness helper tested a handle that
  production never sets when encoding runs in a worker, so a perfectly healthy encoder reported
  *not ready*. It had no callers, so nothing noticed.
- Querying an unknown CAT7 category returned "searched everything, found nothing" — indistinguishable
  from an empty corpus. It now says the category does not exist and names the valid ones.
- A query that triggered the encoder's own load reported `kernel: unloaded` while scoring every
  text with the model. The vectors were right; the label read like a broken install.

**Retrieval quality is unchanged, and this release does not claim otherwise.** At matched
selectivity it measures the same as before this work. What changed is that two thirds of the
mesh's cognition is now reachable at all. A blind held-out benchmark is being sealed by an
independent reviewer before any quality claim is made.

## 0.9.14 — 2026-08-23

**A tenant could report the wrong version of itself, and on Windows it always did.** If XMesh was
installed under a path containing a **space** — or anywhere on **Windows** — it started normally,
authenticated normally, and answered `GET /api/version` with `"0.0.0"`. Not an error: a confident
wrong answer, from the endpoint whose entire job is telling you what a deployment is running.

- **The cause.** Four places resolved their own location with `new URL(import.meta.url).pathname`.
  That is the URL's *percent-encoded* path and it never decodes, so a space stays `%20` and the
  file is not found. On Windows it also gains a leading slash — `/C:/Users/...` — which resolves
  as relative, so the drive is prepended and the read fails as `C:\C:\Users\...`. Either failure
  alone is enough; the version lookup then falls back to `"0.0.0"`. All four now use
  `fileURLToPath`, which the same files already used correctly elsewhere.
- **Measured on Windows 11, not inferred.** Published 0.9.13, installed anonymously into a path
  with a space, reported `{"version":"0.0.0","checkoutVersion":"0.0.0"}`. The same run with the
  fix reports the real version.
- **`/api/version` gained `treeVerified`.** A checkout deployment reports both the tree it started
  from and the checkout's current head; a package install reports neither. Exactly one being
  present means the running tree was never captured — nothing can show which code the process
  loaded — and that state used to be indistinguishable from a healthy one.

**A post-deploy check that asks your tenants what they are running.**
`scripts/deployed-version-guard.mjs` reads an inventory of tenants — where each one is, where its
credential lives, who owns it, never a credential itself — and fails unless each reports the
published version with no restart owed. It never restarts anything: tenants belong to whoever runs
them. `--require-all-under` additionally fails if a tenant exists on the host and is missing from
the inventory, because "all tenants are current" is a claim about a population.

**The supply-chain gate now runs before anything ships, against the artifact that ships.** Both
container images are built from the exact `npm pack` tarball the release publishes, sized against
a budget in compressed OCI bytes, and scanned — all before the version is committed, tagged,
pushed or published. The published tarball's integrity is then checked against the one that was
gated. Release evidence lands in `release-evidence/<version>/`.

## 0.9.13 — 2026-08-23

**One vulnerability finding, zero critical.** The container an enterprise scans before it runs
anything went from 41 CRITICAL/HIGH two releases ago to a single tracked HIGH.

- **The remaining critical was unreachable code, so it was removed rather than excused.** Node
  bundles its own OpenSSL (3.5.4) and never maps the system `libssl3`. Verified against a **live
  process** with the runtime, `onnxruntime-node` and `transformers` all loaded: `/proc/self/maps`
  holds `libc`, `libdl`, `libgcc_s`, `libm`, `libonnxruntime`, `libpthread`, `librt` and
  `libstdc++` — no `libssl`, no `libcrypto`. The base ships 3.0.18 against a 3.0.19 fix upstream
  has not published, so the finding could not be patched; only not-shipped. The image now deletes
  those libraries **and their stale `dpkg/status.d` records**, which otherwise asserted an
  installed package whose files were gone. The build **fails** if anything needed them, and
  proves HTTPS still works without them.
- **`adm-zip` removed** — `onnxruntime-node` uses it to unpack native binaries at *install* time,
  which has already happened by the time the image exists.
- **`sharp` stays, and the attempt to remove it is why.** It is image processing pulled in by
  `@huggingface/transformers` while XMesh embeds text, so it looked prunable — and removing it
  breaks the build, because `transformers.node.cjs` requires it eagerly. An earlier hand-test
  appeared to pass only because it resolved a different entry point than the runtime uses. It is
  the one remaining finding, tracked with an expiry and the upstream fix named
  (`sharp@0.35.0` via a `@huggingface/transformers` bump). **A silently degraded neural path is
  the exact defect this image already carries a gate against.**

```
0.9.11  node:22-slim   41 findings   5 critical
0.9.12  distroless      9 findings   1 critical
0.9.13  hardened        1 finding    0 critical
```

Unchanged: no shell in the runtime image (`docker exec … sh` will not work; `node` is present),
non-root, one volume at `/state`, and the neural-load gate in the build.

Pre-1.0 evaluation software. 910 tests, typecheck clean.


## 0.9.12 — 2026-08-23

A supply-chain release. The published container is what an enterprise scans before it runs
anything, and ours carried 41 CRITICAL/HIGH findings — almost none of them from code we wrote.

- **The image is distroless, and findings drop from 41 to 9.** Scanned at its published digest,
  the 0.9.11 image reported **41 CRITICAL/HIGH: 30 in Debian OS packages, 9 in the npm bundled
  into the base image, 2 in our own dependency tree** — five criticals, four of them *unfixed*
  `perl-base` and `zlib1g`. A Node runtime does not use perl, a shell, or a package manager: npm
  is an **installer**, used once and never again, yet carried into the image by copying
  `node_modules` wholesale. Four of the five criticals had no fix to apply and could only be
  not-shipped. The build is now multi-stage — install, prune what cannot execute here, prove the
  neural path loads — onto a distroless runtime with npm removed afterwards.

  | image | CRITICAL/HIGH | critical |
  |---|---|---|
  | `node:22-slim` (0.9.11) | 41 | 5 |
  | distroless | 18 | 2 |
  | **distroless, npm removed** | **9** | **1** |

  287 MB on disk, down from 315. Healthy in 2.2 s, `uid 65532`, and `ready:false` — correct for
  an image that bundles no mind.

  **There is no shell in the runtime image.** `docker exec … sh` will not work; that is the point,
  and `node` is present for anything you need to run inside.

- **`scripts/supply-chain-gate.mjs`** — SBOM, vulnerability scan, size budgets and release
  evidence in one command. Gates: amd64 ≤ 350 MB, arm64 ≤ 550 MB, no more than +10% against the
  recorded prior release, zero Critical, zero fixable High, and an **expiring** exception for an
  unfixed High — an expired exception fails the gate rather than being forgotten. Evidence lands
  in `release-evidence/<version>/`: a CycloneDX SBOM, per-platform scan JSON, and a summary.
- **`scripts/release-smoke.mjs`** — a clean-room walk of the public developer journey against the
  **published** artifact: install into a temporary prefix and HOME, boot, the ready/grounded
  contract, a real MCP stdio session with all seven tools, commissioning, retrieval **by id**, and
  the CLI's evaluation marker. Wired into `prepublishOnly`, where it packs *this tree* — at
  publish time the version being shipped does not exist on the registry yet.
- **The EOD report carries the licence tier.** Since the host-level evaluation refusal was removed
  in 0.9.11 there is no technical enforcement of the paid boundary, so visibility is the whole
  mechanism. A boot warning scrolls away; the blotter is opened deliberately.

Still open and stated rather than summarised away: seven `libssl3` findings from the distroless
base (six awaiting a fixed base, one with no fix, time-bounded exception recorded), and `adm-zip`
and `sharp`, both reached through `@huggingface/transformers` and fixable only upstream.

Pre-1.0 evaluation software. 910 tests, typecheck clean.


## 0.9.11 — 2026-08-23

Licensing enforcement corrected, the coding-agent path documented, and a Docker image that
exists and has been run.

- **The one-unlicensed-deployment-per-host refusal is removed.** It existed to stop several
  production team roots on one host serving several teams on one payment, and it could not: the
  registry lives in `os.tmpdir()`, so every container gets its own and the dodge was `docker run`
  twice. Meanwhile it *did* refuse a paying customer trialling a second root — the count never
  checked whether the other deployment was licensed — and on Linux it crossed user accounts,
  `/tmp` being shared. **A rule that misses every case it was written for and catches only honest
  users is worse than no rule.** Unlicensed roots now evaluate; unauthorised production use is
  governed by the licence agreement.
- **Evaluation status is visible instead.** The code claimed unlicensed deployments ran
  "watermarked" and **there was no watermark** — one boot warning that scrolls away. Now
  `GET /api/version` carries `tier`, `xmesh status` prints `· EVALUATION (unlicensed)`,
  `/api/license` is unchanged, and the runtime's own run-evidence artifacts are stamped.
  Deliberately **not** the doer's deliverable: that is the customer's document and the runtime
  does not edit it.
- **`xmesh-mcp` resolves which tenant owns a port.** `XMESH_URL` says where the bridge talks and
  never which passcode to bring, so pointing it at a named team root authenticated with
  `~/.xmesh`'s credential and returned 403. On a 403 against a **local** URL — and only when
  `XMESH_PASSCODE` was not set explicitly — it now reads the root from the listening process and
  **says on stderr which tenant answered**. A bridge that adopts a credential nobody pointed it at
  must never do so silently.
- **The README documents the coding-agent path.** It described only `@sym-bot/mesh-channel` —
  how an agent becomes a sovereign mesh *peer* — and never `xmesh-mcp`, which is how a coding
  agent actually commissions work and collects results. There is now a five-minute MCP-first
  path, the two integrations are distinguished as the different jobs they are, and the HTTP API
  is kept as reference rather than as the first thing a developer meets.
- **A Dockerfile that exists.** The README has documented one since before 0.6.5 and there was
  none in the repository. It installs a **pinned** version from npm rather than copying a working
  tree, runs non-root with the state volume chowned, and states what a container does *not* get —
  no LLM, no peers, no Station. Built and run before shipping: healthy in 1.0 s, `{"ok":true,
  "ready":true,"grounded":false}`, state on the volume, `uid=100(xmesh)`.
- Corrected in the status table: **Enterprise Federation is shipped and licence-gated**, not
  planned — the cross-organization endpoints answer HTTP 402 without a production Team licence.
  The boundaries section had claimed federation *and* licence-v2 enforcement were unshipped;
  both were already running.

Pre-1.0 evaluation software. 908 tests, typecheck clean.


## 0.9.10 — 2026-08-22

Two MCP tools that never did what their descriptions promised, and a licensing section in this
README that described a model we stopped shipping three minor versions ago.

- **`mesh_ask` returned the citation key of the answer, not the answer.** Its description promises
  "a SYNTHESIZED answer where every claim is cited … never a black box". It mapped the per-agent
  claims and `synthesis.key`, and never `answer.prose` — which the stream has carried all along.
  So an agent asking the mesh a question received twenty-five content hashes, the raw claim
  headers, and no answer. The mesh knew; the tool did not say. It now returns the prose, the
  synthesis mode, the mind that composed it, and the cost — which the CLI has always printed.
- **`mesh_recall` has never returned any content, in any mode.** It read `h.focus`; the server
  sends `text` (the matching category's text), `category` (which axis matched) and `agent`, and
  has **no `focus` key at all**. Every call since it shipped answered with bare content hashes —
  and there is no fetch tool on this surface to resolve them, so recall was unusable on its own
  rather than merely thin. Long hits are trimmed with the amount cut stated.
- **The MiniLM path was unreachable from the agent surface.** Neural recall lives behind
  `mode=neural`; the tool's enum offered only `match|semantic` while describing `semantic` as
  "embedding". All three modes are now offered and described for what they actually do.
- **README licensing was three versions out of date and contradicted the code.** It said *"Current
  0.6.5 builds still enforce the legacy token's seat cap; licence v2 **will** replace that
  enforcement… Until licence v2 ships, do not mistake the legacy `seats` field for the current
  commercial pricing model."* Licence v2 shipped around 0.7.x. Anyone reading this repo for the
  contract found it saying the opposite of what the product does. Corrected: v2 is the shipped
  model — one flat licence per production team root, unlimited humans and agents, no metering,
  verified locally and offline, with a root's identity derived from its canonical path so a
  copied root is refused. `seats` survives only for existing v1 licences through their expiry.
  **The free allowance is a deployment, not a seat.**

- **The README's own contract tests were pinning its staleness.** One asserted the literal
  sentence *"Current 0.6.5 builds still enforce the legacy token's seat cap"* — a claim naming a
  version, which expired when v2 shipped, so the test required the README to state something
  untrue and failed the moment it was corrected. Another pinned `{"ok":true,"ready":false}` as the
  first-run example, holding the README on a response shape that changed in 0.9.9. Both now assert
  the **property** — that legacy v1 seat enforcement is disclosed, that the health example shows
  and explains its fields — rather than the wording. The first-run example is corrected, and notes
  that a fresh install commonly reads `ready:true, grounded:false`: it can do work and has no peers
  to ground in yet, which is an isolated deployment rather than a broken one.

Pre-1.0 evaluation software. 908 tests, typecheck clean.


## 0.9.9 — 2026-08-22

Four defects, all found by *using* the product rather than reading it — two by walking the
agent-first (MCP) path end to end, two reported from the app-team tenant with reproductions.

**One behaviour change worth reading before you upgrade:** `GET /api/health` now returns
`{ok, ready, grounded}`. `ready` no longer requires a live mesh, so anything gating on it will
start on an ungrounded deployment. Use `grounded` if you meant the stricter thing.

- **`ready:false` on a server that was doing the work.** A tenant accepted a commission in 7
  seconds and delivered a 22 KB review 8 minutes later, reporting `ready:false` throughout. The
  predicate required a live bridge *and* a mind; that tenant has a mind and no live peers — not a
  broken deployment but an **isolated** one, which is exactly what containerising this server
  produces. A readiness field that says false on a working server is bound to something other than
  readiness, and anything gating on it either never starts or pages forever. Now: **`ready`** = can
  it accept and run work; **`grounded`** = is it on a live mesh, as itself. The empty-container
  failure `ready` was added to catch is still caught, named for what it is. `/api/health` and
  `/api/readiness` had been computing that answer with two different expressions — only one knew
  that a *refused observer* reads `live` while seeing another tenant's peers. One shared predicate.
- **The CLI made you guess which tenant a port belongs to.** `XMESH_URL` changes where the CLI
  talks, not where it reads the passcode, so pointing it at another tenant's port 403'd with an
  error naming three environment variables and saying which of them applied. On a 403 against a
  **local** URL — and only when `XMESH_PASSCODE` was not set explicitly — the CLI now reads the
  root out of the listening process's own environment and uses that tenant's passcode, **saying on
  stderr which tenant answered**. Using a credential you did not point it at must never be silent.
  If that fails too, the error names the pid and the root.
- **`mesh_missions` returned every mission ever, and could not fit in an MCP response.** Its own
  description says "live or awaiting a human verdict"; it had no filter at all. On a working
  deployment: 63 missions, 532 commissions, **125,823 bytes — past the MCP result limit**, so an
  agent calling it got an error rather than a long answer. It also disagreed with the CLI's
  `missions` (8 rows) on the same data. Now filtered to the same set the CLI uses (**28,205
  bytes, 13 missions**), and it takes an `id` — because filtering alone would break following your
  *own* mission, which tears its world down exactly when you want the result.
- **`mesh_board list` returned every item's whole history — 40,880 bytes.** The board never
  deletes, so action logs grow without bound; several carry over 3,000 characters of prose. The
  last action is trimmed to 240 characters and **says how much it cut**. New `detail` action
  returns one item's full history, and `list` takes a `status` filter and reports how many it hid.

Pre-1.0 evaluation software. 905 tests, typecheck clean.


## 0.9.8 — 2026-08-22

**The daemon now starts in about a second.** Before this, a restart could leave it unreachable for
over two minutes — port closed, nothing listening, no error logged. Measured on the same host,
same state, before and after:

```
before   4.9 · 5.3 · 6.0 · 7.1 · 7.3 · 72.7 · 101.4 · 116.2 · 164.8 s
after    1.0 · 1.0 · 1.1 · 1.1 · 1.1 · 1.1 s
```

- **One restored agent was reading the entire mesh history before the port opened.** Restoring a
  deployed agent seeds it from canon, and that seed named *every store that has ever existed*:
  `readCanonCmbs` opens and parses every CMB file in every store it is given — **35,116 files,
  420 MB, across 1705 directories** on a working deployment. The reads are synchronous, so
  running it detached bought nothing; the event loop was held either way. **88.8 seconds of
  synchronous file I/O across 42,310 calls in the first 92 seconds of a boot.** The restore now
  happens after the server is listening and answering.
  This does **not** reduce the cost of that canon read — a recall that scales with all history is
  a design question, and narrowing what canon recalls would change what an agent knows. What
  changes is that the daemon no longer looks like a dead host while paying it.
- **The startup log line cost 30 MB to print.** `observed cmbs: N` read and parsed every CMB of
  every active node — 3,893 files — to produce one number, inside the listen callback. A CMB's key
  *is* its filename, so the count now lists directories and opens nothing.
- **`XMESH_TRACE_SYNC_IO`** (off by default): every synchronous read or write slower than the
  threshold reports its **path, size and duration**, plus a total and the worst paths ten seconds
  in. Set it to `1` for a 50 ms default, or to a number of milliseconds. A stack sample tells you
  what *kind* of work blocked; this tells you which *bytes*. It exists because three plausible
  diagnoses of the above were published before one survived a measurement, and every one of them
  failed for the same reason: the filename was never in the evidence.

Pre-1.0 evaluation software. 896 tests, typecheck clean.


## 0.9.7 — 2026-08-22

A performance release, and an honesty release. Every number below was measured on a real
deployment, not estimated.

**The cold start could block for over a minute.** After a restart the daemon opened its port,
logged `API on http://localhost:8787`, accepted connections — and answered nothing. Repeated
identical cold starts against the same state gave 4.9 s, 5.3 s, 7.3 s, **72.7 s** and **101.4 s**.
One production restart took roughly four minutes. The failure shape is the damaging part: the
server does not crash and does not refuse, so it looks like the network is broken rather than the
product.

- **The embedding cache is 3.9× smaller and 9× cheaper to write.** `MAX_CACHE` bounds *entries*,
  and the comment beside it claimed that bounded the file — but a 384-dimension vector printed as
  JSON decimals is ~8 KB, so 20,000 "bounded" entries were **154 MB**. Worse, the whole map is
  rewritten 3 seconds after *any* new embedding, and that write was synchronous: **786 ms of
  blocked event loop, repeatedly, on a busy mesh.** Vectors now persist as the base64 Float32 they
  already are in memory — 154 MB → **39.6 MB**, serialize 591 ms → **65 ms** — and the write
  leaves the event loop. Both encodings are read, so an existing cache keeps working and
  re-encodes as it flushes: no migration, no cold cache after upgrade. Float32 is not a precision
  loss; the extractor returns `Float32Array`, so the decimals were a widening on the way out.
- **The event loop reports its own stalls.** A freeze leaves no trace of itself — by the time
  anyone looks, the process is healthy and the log says it started fine. The daemon now warns
  `EVENT LOOP BLOCKED for Nms` when the loop is held past `XMESH_LOOP_STALL_MS` (default 1000),
  escalating only on a new worst so a slow host reports a deterioration rather than filling the
  log. It does not say *which* call blocked; it says one did, and for how long, which is the fact
  that was missing.
- **`GET /api/version` spawned `git` twice, synchronously, on every request** — 32–164 ms of
  blocked loop on the endpoint the station and every seat poll. Cached for 5 s.
- **Archive retention covered 9% of what it exists to bound.** The store it sweeps had grown to
  1705 directories; the rule matched 156 of them (105 MB) and missed the largest class outright —
  359 `reoffer-*` crew stores holding **681 MB**, plus `resume-*`. A reoffer doer *is* a mission
  crew; it was offered twice. The role suffix is now required as well as the prefix, so widening
  cannot reach a standing node whose name merely starts with one of those words.
- **The archive reports its own size, and can be given an end.** The sweep *moves* stores, so it
  bounds the scan surface and reclaims no disk — and nothing reported how large it had become.
  Every sweep now logs the count and MB. Expiry is **opt-in and defaults to never**:
  `XMESH_ARCHIVE_EXPIRE_DAYS` turns deletion on for an operator who has decided their own answer.
  These directories hold CMBs, and validated cognition does not expire — that horizon is a
  retention decision, not a default to inherit from a patch release.
- **`GET /api/version` reported the version on disk, not the version running.** On a checkout
  tenant that becomes wrong the moment anyone bumps `package.json`: a tenant answered "0.9.6"
  while running five commits behind. Captured once at start now, with the live read exposed
  beside it as `checkoutVersion`, and a bumped file counts as a restart owed on its own — the
  git comparison is silent for a package install. `xmesh status` prints `0.9.5 (disk 0.9.6)`.
- **`xmesh review Docs/foo.md` works.** The API requires an absolute path because the doer is
  another process with another cwd — but the client is the only party that knows the caller's
  cwd, and it handed back a rejection instead of using it. The CLI and the MCP bridge now
  resolve; `xmesh assess --inputs` had the same shape. The server's refusal names what it got.

Pre-1.0 evaluation software. 896 tests, typecheck clean.


## 0.9.6 — 2026-08-22

A correctness release. Four MCP tools were shipped, silent and wrong; the abandon endpoint
diagnosed the wrong cause; a fresh install could run for weeks with nothing ever paid and no
sign of why; and the test suite was writing to the operator's own state.

- **`mesh_canon` returned an empty list on every call against every real deployment.** It read
  `r.canon || r.rows || r`; the server sends `{count, cmbs}`; neither name matched, so it fell
  through to the response object and `Array.isArray` flattened it to `[]`. Verified live: the old
  expression yields 0 entries where the mesh holds 3. The test covering it mocked a body the server
  has never sent, so it passed forever while the tool was broken — the mock now produces the
  server's own shape and fails without the fix.
- **`mesh_ask`'s `ground` and `mesh_recall`'s `field` were silently ignored.** Both sent `field=`
  where the server reads `category=`, so every call since those parameters were added has run
  ungrounded. No body or path assertion can catch a wrong *parameter name*, so the mock now records
  query strings and two tests assert them.
- **`xmesh research` authenticated against the wrong tenant.** It read `~/.xmesh/passcode`
  unconditionally, ignoring `XMESH_STATE_DIR` and `XMESH_TEAM_ROOT` — so on a team-root tenant it
  used another tenant's passcode while every other verb worked. It now resolves the state dir the
  way the CLI and the MCP bridge already did.
- **Abandon says which refusal it is, and a mission id means something.** The rest-states-only
  restriction was lifted on 2026-08-18, but one refusal message still covered two unrelated causes
  and asserted a third thing that had stopped being true — "stop the worker to interrupt running
  work". `xmesh abandon mission-…` hit the unknown-key path, because a mission id is not a
  commission key and never can be, and the operator was told they had no control over running work
  at all. Now 404 names the missing key and points a mission id at the teardown that abandons every
  open commission a world owns; 409 names the booked status it actually found. The CLI routes a
  mission id to that teardown.
- **One warning per boot when there is no critic.** Without `XMESH_MISSION_CRITIC=1` no mission
  spawns a reviewer, so every delivery matches `no-counterparty` at EOD and nothing is ever paid —
  while the deployment looks perfectly healthy doing it. The trade model's honesty rests on a
  counterparty existing, and its absence was silent. A fresh install gets that default.
- **The test suite no longer writes to your live state.** `test/_setup` overrode ten store paths
  where roughly thirty exist; everything else — sessions, users, the passcode, room policies,
  review panels, conversations, SVAF profiles, lifecycle, and the deployed-agents roster — resolved
  to the operator's real files. It now sets `XMESH_STATE_DIR` and `SYM_STATE_DIR`, redirecting every
  derivation at once, including ones added later. `test/sessions.test.ts` hardcoded a home path and
  truncated the live session file; it now resolves the path the way the server does. (Found by an
  independent release review running in a restricted sandbox — this machine allowed the writes that
  sandbox refused, so the suite was green for the wrong reason.)
- The run-evidence log records built-in enumerations — Grep, Glob, a directory listing — as SEARCH lines with their result tails, and the critic's rubric names a SEARCH-grounded count as measured, not authored (dev-team-3: an honest Grep count was objected to as fabricated provenance because the log was blind to it).

## 0.9.5 — 2026-08-19
- Per-room daily budget (dev-team-3 H2b), OPT-IN: `dailyBudgetUsd` on the room policy (or `XMESH_ROOM_BUDGET_USD` for every room without its own line). Spend is the day's asks and fills with a known usd (UTC day); past the line an offer or ask answers 402 with the numbers, and a validator's explicit `overBudget` lets it through. `GET /api/rooms/:name/budget`. Station: the line in the room's policy, ASK ANYWAY / APPROVE & RUN for validators. No line → nothing changes.
- The operator CLI ships in the package (dev-team-3 H4): `xmesh <verb>` — status · soak · ask · offer (lints first, `--force` to offer with warnings) · lint · missions · detail · eod · breaks · rule · replay · abandon · register · proposals · review · assess · workers — against the tenant the MCP bridge talks to; `--json` for scripts. An unknown word now exits instead of starting a server.
- `POST /api/recipes/review` and the MCP tool `mesh_review` — "review this file / diff" as one mission: VERDICT, numbered findings each quoting the line, the author's questions answered, NOT CHECKED stated; the critic's grounds fixed (a missing quoted line, a quote not in the target, a restructuring or rewording, a missing NOT CHECKED — nothing else). Never changes the target.
- Ask cost in words (dev-team-3 H2a): every ask reports `cost` — minds that ran, tokens, usd over the lanes that report one (the claude CLI's own `total_cost_usd`) — on the answer, the done event and the room's conversation turn; the station shows "this ask: N minds · tokens · usd" after, and "last: …" beside ASK before the next one.
- Escalating an answer to a mission (H2c) carries the answer's synthesis key, its citations and the prose so far into the mission as inputs — the fill starts from what the mesh already knows.
- Ask lint (dev-team-3 H1): the four properties of a good brief — work not a question, claims under test, a provenance label, a named null — checked at the offer (`POST /api/commissions` and `/api/worlds`, `?lint=1` or `lintOnly` for the report alone) as warnings, never refusals; a containment-only brief shows the scaffold that passes its checks; a reused deliverable path is named. The report rides the commission (`lint`) so the blotter can read break rate on linted briefs. Station: the report under the CAT7 form with RUN ANYWAY.
- `GET /api/soak` (dev-team-3 H3): fills since restart, matched / breaks / no-counterparty, reaped / re-offered / double-staffed / silent deaths, the clean streak against the 20-fill rule, and the last defect. Station: one line on ACTIVITY.
- Standing mark persists the moment it is set (the roster was written before the mark, so the last-staffed worker — the critic — lost it across a restore).
- `restartOwed` on `GET /api/version` fires only when the commits between the running tree and HEAD touch the runtime — a docs-only or CLI-only commit no longer asks every tenant owner to restart.
- Station 0.9.5: the lint report under the CAT7 form with RUN ANYWAY; the soak line on ACTIVITY; ask cost before and after; the room's daily line in the minds popover with ASK ANYWAY / APPROVE & RUN.

## 0.9.4 — 2026-08-19
- Harness proposals: a trial is counted on the harness-stamp slot the text produces (the template slot hashes both templates together), and says which stamp it counts; a later apply on the same resource marks the earlier trialling proposal superseded.
- Only requested/accepted work holds a worker: a paused or blocked mission resumes by a fresh worker, so its crew is released (an idle tenant no longer reads "3/4 mind slots in use"). `POST /api/commissions/abandon-stale` books rested, unstaffed rows as dismissed in one sweep.
- The activity stream stamps a record with its own authored time, never the observer's clock.
- Register rows keep their originating evidence and carry the latest disclosure beside it.
- Standing workers (hand-staffed or recipe-staffed capacity) survive a daemon restart: the standing mark rides the worker roster and is restored with the worker. It lived only in memory, so a restart brought the worker back unmarked and the idle reaper took it five minutes later (dev-team-3, the eng-northbank pair).
- `GET /api/commissions/:key/evidence` — the doer's run-evidence log (READ/WRITE/RUN/TOOL steps with what each returned), parsed and raw, with the fill's usage — what the station's process audit shows first.
- `GET /api/recipes/integration-assessment/defaults?engagement=` — the engagement's workspace, last call on disk, next call, inputs dir, whether the Camel dry-run tool is live, and the last fill's cost, so the assessment form is one click.
- `POST /api/rooms/:name/retire` — archive a room with no residents and nothing open or resting (refused otherwise).
- `/api/judgement` records carry the commission they belong to.
- Station 0.9.4 (served from the checkout): verdict controls only on breaks, settled bookings stamped; one key convention (8-char prefix); process audit reworked; plain ask and cognition-panel empty states; lineage as a tree; run-assessment prefilled; full-width canvases with a summoned ask pane; register facets; phone layout; type floor and contrast.

## 0.9.3 — 2026-08-19

- **The integration assessment is a first-class mission.** `POST /api/recipes/integration-assessment`
  `{engagement, inputsDir, call, workDir?, notes?, route?, expectClosed?, criticGrounds?}` writes the
  brief dev-team-3 ran by hand four times (inputs read whole; register carried forward by id with
  statuses; cognition map; optional Camel route + dry-run through the tool; DERIVED-BY-DOING/INFERRED
  provenance; verbatim-wording FINDING lines with status/closed_by; mechanical checks), commissions
  it IN the engagement room, and staffs a standing doer (+ critic) there if none is alive.
  `preview:true` returns the brief. Station: RUN ASSESSMENT on the Register view.
- The recipe's pass thresholds scale from the previous call's register — rows carried forward
  (counted from that call's `integration-register.md`, the mesh register only when no file exists),
  two thirds derived by doing, closures only when `expectClosed` says so; the map is checked for its
  structure, never for a sentence that may be false on a later call; the aim is carried verbatim;
  the critic's grounds ride on the brief when given (dev-team-3, eng-northbank calls 5–6: a complete
  fill no longer fails a fixed or inflated count).
- Register: a connector is recommended only for transport-shaped blockers (interface,
  infrastructure, delivery, failure/replay, consistency, preparation; novel rows only on a named
  system) — never for an ownership or decision blocker (dev-team-3: "the spreadsheet has one
  editor, on leave" → camel-file was a category error).

## 0.9.2 — 2026-08-19

- `GET /api/version`: `tree` is reported only when the package root itself is the checkout — a
  package installed inside a parent git repository no longer reports that repository's HEAD.

- **The register is the owner's open list.** A finding has a lifecycle — `status: OPEN|NARROWED|
  CLOSED|REOPENED | closed_by: <role>` on the structured line (the brief teaches it); a later
  disclosure with the same blocker identity UPDATES the row instead of adding a second; the document
  leads with "N open · M closed", lists closed blockers in their own section, heads each row by
  function · owner, prints the owner's question once, and labels commission keys "Source"
  (dev-team-3, eng-northbank, from the owner's chair).
- **Connector-type analysis lands on main.** The complete Apache Camel component catalogue (~340
  schemes) and `analyzeConnector()` — the Camel component recommended for each integration point,
  with rationale and match confidence, mainframe/AS400 → jt400 — were built 2026-07-26 (PR #11) and
  never merged; the station already rendered the field the server never set. Now: every blocked
  step and every register row carries the connector that would close it, from its own words; the
  register document prints it.
- Integration points are classified by the **missing resource**, not the worker's phrasing: a
  provisioning gap (grant, credential, allowlist, egress) is a capability act routed to the platform
  owner; `human-approval` is reserved for a genuine business decision (also from the unmerged 2026-07
  branch — a step phrased "needs approval: cannot reach Postgres" used to go to a business approver).
## 0.9.1 — 2026-08-19

### Resource proposals — the harness evolves as trades (design v2, step 2)
`server/resource-proposals.ts` + `GET/POST /api/proposals`, `POST /api/proposals/:key/commit|rollback`.
A proposal is a content-addressed change to one harness text (`{target, from, to, toText}`), gated
by the room's **learnability mask** (`RoomPolicy.resources`; constitution not evolvable by default,
templates 0.4, rubric 0.6, grammar never), raised as a **corporate action** (four-eyes card; the
proposer cannot vote — solo mode excepted, where the owner ratifies), **applied by compare-and-swap**
on the live text with the archive named by the bytes it holds, **trialled** on the fills that stamp
the new hash (proposer excluded; thresholds from the previous text's measured per-run variance — no
constant N; a counterparty-quiet trial cannot be committed), **committed by a human**, **rolled back
by a new proposal**. EOD refreshes every trial and may only mint a rollback proposal. Verified live
on core-team: the mission-0979b9 rubric refinement, applied, stamped by the next fill, matched.

- **The mesh proposes from failure.** A deep break-RCA (`POST /api/commissions/:key/rca/deep`)
  may append a `## PROPOSAL` block — the target and the full replacement text — to its report; on
  completion it is minted through the same gate as a human's (earned authority at the room's tier,
  four-eyes card, human approval). Station: the HARNESS view — the blotter by harness and every
  proposal with its trial, COMMIT / ROLL BACK.
- `/api/rooms` no longer mints a phantom `<room>--team-…` room per tenant-scoped operator name.

- `GET /api/version` — `{version, tree, startedAt, generation, teamId}`: "is this tenant on the
  release" in one call (dev-team-3 soak, P3).
- Override transitions are logged both ways, keyed by content — an override that was valid and then
  broken by a later edit no longer reverts to the default in silence (dev-team-3 soak, P2).
- **The whole objection travels** (dev-team-3 soak, P1). The critic's reason was cut to 140
  chars at emission and nothing downstream could read the rest. `focus` stays the headline;
  `motivation` carries the full reason (up to 4,000 chars); the critic writes
  `<artefact>.objection.md` beside the artefact; the server records the full text and the break
  RCA quotes the file when it adds to it.
- **Check grammar: a bare needle is malformed, named, and refused at the offer** (R17's other
  half). `CHECK: contains J1` with no path used to take the NEXT LINE as its needle — each bare
  clause ate the one after it; an 11-clause brief parsed as 6 and graded 1/6. Separators are
  horizontal whitespace only; a malformed known kind says what it needs.
- The mind that filled (`mind=<mind>/<model>`) rides the accept, the record, every EOD line, and
  each harness group's `minds` breakdown — same harness, different minds is not one population.
- **Diagnostic work is not a trade.** Review lenses and RCAs (work ABOUT a trade) read as
  `diagnostic` at EOD — listed, never matched, paid or broken; the human rules on the trade, not on
  the review of it (dev-team-3: an RCA-of-a-break was about to book as a break).
- `GET /api/version`: `tree` is the commit this PROCESS runs (captured at start); `checkoutHead` is
  the disk now; `restartOwed` when they differ (dev-team-3).
- **camel-dryrun reaches the mind on a built deployment.** The MCP entrypoint was resolved against
  the module's own path — right from `worker/`, pointing into a non-existent `dist/skills/` from the
  build — and `skills/` was not in the package at all. Resolved from the package root now, shipped
  in the tarball, and a missing entrypoint is a startup warning. `dryrun.sh` matches `expect`
  against non-error lines only (dev-team-3, integration soak).
- **A FINDING is not a blocker** (dev-team-3 integration soak, P1). A doer's customer-side
  findings were routed into the ask-the-mesh/fork path on the first one (a ghost fork, eleven of
  twelve lost). Now: all findings travel; with the brief's deliverable on disk the run is a
  DELIVERY with findings (graded, broken at EOD if a clause fails); with nothing on disk it is a
  finding booking — never a mesh ask, never a fork. Forks for genuine our-side blocks are directed
  to the operator so they exist. The evidence log is written on a stopped run too.
- **The register keeps the owner and the question.** Structured finding line
  `FINDING: <blocker> | function: … | owner: <role|UNKNOWN> | closes: <question> | evidence: "…"`,
  taught by the brief, stored verbatim, rendered; finding lines may be 600 chars.
- **A replay is staffed once** (dev-team-3, P1). A goal-bound resume/replay worker accepted the
  parent's request too (same text, same room) while the room doer took the replay — one ask, two
  minds, both paid. A goal-bound worker now takes only its goal (and its shards), and a blocked
  booking refuses a stranger's late fill — or any fill once a replay exists — recording it instead.
- **The evidence log records MCP tool calls** (`TOOL: mcp__<server>__<tool> <args> → ok|error ·
  result tail`) — the dry-run invocation a tool-backed fill rests on was invisible, so the critic
  called every such fill unsubstantiated. The rubric reads a TOOL line as a run.
- Register: the disclosed finding is kept at 600 chars on the record too (it was cut to 200 after
  the worker had kept it — `closes` truncated, `evidence` lost), and an entry's identity is the
  blocker's own words (sixteen distinct findings had merged into one because the subject came from
  the brief's first quoted string).
- **A terminal commission is not work.** A worker joining a room with history accepted a request
  for an already-completed commission (the registry refused it silently), redid the work, and was
  reaped mid-run while the room's new request fell to the 90s fallback. A worker now reads the
  registry before accepting and skips terminal commissions aloud. **Standing workers** staffed by
  hand (`POST /api/workers`) are the operator's capacity and are never reaped for being unnamed
  by a live commission — the reaper is for crew (dev-team-3, eng-northbank).
## 0.9.0 — 2026-08-19

### Resource overrides + membrane version chain (design-resource-proposals.md §2, §6.4)
- The four harness texts can be overridden per tenant under `<state>/resources/` — `constitution.md`,
  `rubric.md`, `template.claude.md`, `template.codex.md` (with `{{WORK}}`/`{{CRITERIA}}`). A valid
  override replaces the default whole; an invalid one is ignored and logged once; the harness stamp
  hashes whatever shipped, so a deploy and an override are judged the same way on the blotter. This
  is Improve without a release — the step the proposal loop needed.
- Adaptive SVAF profiles carry a content-addressed `stamp` and `prev`; every weight change is
  appended to `<state>/svaf-profiles/<node>.jsonl`; `profileAt(node, stamp)` is rollback as a read.

### From the mesh's own Autogenesis gap analysis (mission-257d4b): three defects closed
- **A review lens's judgment now lands on the fill it challenged.** The adversarial-review
  lenses closed as their own bookings; "JUDGMENT: challenge" never reached the challenged
  commission's objections — the channel EOD matches on had never fired. challenge → objection,
  "no challenge found" → approval, "unable to test" → nothing; never a self-verdict.
- **One payout path.** The fill-time credit on the doer's own checks is gone; authority is paid at
  EOD when a counterparty matches, or by the human's annotation. Three paths had disagreed on one
  fill. A fill matched and paid at EOD then dismissed by the human surfaces as a **late break**.
- **The evidence log records what a command RETURNED** — exit status and an output tail — not only
  that it was issued; the critic's rubric reads a run with no recorded output as cannot-verify.

### Cost on the trade — token accounting per fill
Reading Anthropic's "Maximizing the value of your Claude Code sessions" onto XMesh: every doer and
critic is a fresh session, and nothing recorded what a fill cost. The worker now reports usage on
its completion (`usage i=… c=… o=…[ usd=…]`, from claude's result event / codex's turn.completed);
the commission keeps it; every EOD line carries it; `GET /api/eod/harness` reports cost per harness
and **usd per matched trade** — the number a harness change is judged by. Three savings the article
prescribes, each now measurable: the brief template comes FIRST and the mission text LAST (a shared
cache prefix across fills, 0.1x); doers may delegate wide reads (`Task`) so a survey does not ride
every later turn; constitution v1.2.0 adds (j) token discipline — delegate wide reads, quiet noisy
commands, write long output to a file.

### Break RCA — Reflect over a break
A blocked step had a root-cause report; a BREAK (doer delivered, critic objected, EOD split them)
had none. Every break now gets a tier-1 Reflect written beside the artefact (`<name>.break-rca.md`,
also on `GET /api/commissions/:key/rca`): the fill, the objection, the harness it ran on, and an
**implicated resource** — criteria / constitution / rubric / artefact / unclear — that names the
evidence it rests on. `POST …/rca/deep` on a break hands the mesh the tier-2 question: which side
does the artefact support, and which one resource change would have prevented the split. Station:
"WHY THEY SPLIT" on objected cards. Also: a verdict carried by an operator node (a remix of the
critic's) no longer counts as a second counterparty; identical objection text is one objection.

### Harness stamp — the trade records the harness it ran on
Reading Autogenesis (arXiv:2604.15034 §3.1: resources need "explicit state, lifecycle, and version
lineage" or evolution cannot be audited) onto XMesh: a fill depended on the brief template, the
constitution, the critic rubric, the check grammar and the runtime version, and none of that was
on the booking — two replays of one text could run on different harnesses and read as one
experiment. Every accept now carries a content-addressed stamp (`harness t=… c=… r=… g=…`,
`src/harness.ts`); the commission keeps it with the runtime version; every EOD line carries it;
`GET /api/eod/harness` reads the blotter BY HARNESS — the Evaluate objective for any change to
the harness, measured on real fills. Our own `CONSTITUTION_VERSION` was a hand-bumped string;
hashes cannot be forgotten.

## 0.8.0 — 2026-08-18

### The critic now verifies — first verdict, first matched trade
Cross-vendor verification (`XMESH_MISSION_CRITIC=1`) had never produced a verdict on any
deployment: 200 deliveries, 0 approvals, 0 objections. Six independent breaks, each measured
live and each fixed:
- **Unreachable.** A doer broadcast its completion; a fresh critic's skeptical membrane admits
  nothing into an empty store, so it never saw one. The doer now DIRECTS its completion to every
  critic in the room (directed CMBs land unconditionally).
- **Never staffed on hosted worlds.** A mission a standing room volunteered for spawned no critic
  at all. Membrane-hosted worlds now staff the same critic a crewed world does.
- **Hosted by members that cannot fill.** A rules watcher with no working mind (`retention-node`)
  won every membrane offer and the mission sat unfilled until a heal rescued it (459 refusals,
  zero volunteer summons). Only members that can summon a filler may host (`canHost`).
- **Killed at booking.** Booking-on-fill released the crew — critic included — at the instant of
  the completion it existed to review (exit 143, 64s). Booking now releases the DOER and holds
  the critic in a **settlement window** (`XMESH_CRITIC_SETTLE_MS`, default 5 min) until it
  verdicts; a verdict releases it at once.
- **Codex model the account rejects.** `gpt-5.6-sol` 400'd on every run ("not supported with a
  ChatGPT account"); 45 critics, zero completed turns, hidden behind a benign cache warning. The
  codex model is now the account's (`--codexModel`, else `~/.codex/config.toml`, else codex's own
  default) and the real API error is surfaced from the JSON stream. A claude critic now honours
  its decorrelated `--model` (it silently ran on the doer's model).
- **Verdict cited a key the server did not index.** The critic cited the completion's lineage
  parent — on a directed delivery, the doer's previous progress note. It now cites the commission
  key, and the server matches a verdict against any key it knows for the completion.
Also: mission join-matching no longer treats path segments as content (two unrelated asks under
one team root joined the same world); a hosted world holding only its critic is still unfilled.
Verified live: `mission-ae8d91` — doer delivered 85s, critic APPROVED, world settled on the
verdict, EOD matched and paid. Founder direction (2026-08-18): investigate the critic and fix it.

### Booked is immutable, both directions; restored critics follow the deployment; check grammar
- A late completion on a **dismissed** commission is recorded on the row and refused — the
  book is closed; redo is a replay. (dev-team-3: `dismissed → completed` 706s later under the old
  "grounded work outranks a stale verdict" clause.)
- A restored critic re-resolves its **mind, model and membrane from the running deployment**
  (`critic-spec.ts`), not from the roster snapshot — a persisted codex critic under a
  claude/sonnet deployment could not verdict, and a restored critic ran with no weights at all.
- Check grammar: `CHECK: not-contains <path> <needle>` and `CHECK: contains-at-least <path> <N>
  <needle>` (both substantive). An **unknown** check kind now FAILS with a reason instead of
  silently vanishing from the tally.
- Runbook: a plist edit is not picked up by `launchctl kickstart -k` — bootout + bootstrap, then
  verify with `launchctl print` (README, provisioning doc).

### Replay for the operator; content-addressed CMB lookup
- `POST /api/commissions/:key/replay` — any BOOKED commission (delivered, rca, validated,
  dismissed) replays as a new trade citing it; the booking is never rewritten and `replayedInto`
  points at the newest replay. Approval-triggered replays now stamp it too.
- `findDaemonCmb` probes `<key>.json` per node store instead of parsing every CMB of every node:
  `/api/judgement` 20-27 s → under 1 s on a 1,569-node host.

### Withdraw books; the review desk is tenant-scoped
- `POST /api/commissions/:key/abandon` books ANY open commission as withdrawn (reason on the
  record) — an accepted-but-pointless commission had no operator control and held its crew.
- Disk recovery books only against an artefact written AFTER the ask: a prior trade's file at
  the same path left the new trade open (it had booked "delivered — recovered from disk" for
  work its worker never did).
- Rule a booking by EITHER key: `POST /api/lifecycle/:key` accepts the commission key (what the
  blotter and breaks queue carry) as well as the completion CMB key; `GET /api/eod/breaks` names
  each break's exits (validate / dismiss / replay).
- Unknown check kinds are refused at OFFER time (`POST /api/worlds`, `POST /api/commissions`
  → 400 naming the clause and the known kinds), not only at grade.
- `POST /api/session` on a wrong credential names the deployment that refused it, so "another
  tenant's passcode" and "wrong passcode" are distinguishable on a multi-tenant host.
- The review desk room is tenant-scoped (`x-review--<teamId>` under a team root) like the
  operator identity: two team roots on one host shared `x-review` and filled each other's
  review requests. `XMESH_REVIEW_ROOM` still overrides.

### Ask: short terms count, and the mind answers a zero-record question
- "what is sym.bot?" reached every agent with zero query terms — `sym.bot` tokenised to two
  ≤3-char words and both were dropped — so relevance was 0.000 for all 31 and no one answered.
  Compounds (`sym.bot`, `sym-bot`) are one term plus their parts; three-letter content words
  count (`mmp`, `api`, `llm`); function words still don't.
- With a mind configured, an ask that no mesh record cleared still gets an answer — from the
  declared reference sites, the web and the mind's own knowledge, labelled as the mind's — instead
  of "No grounded contribution cleared admission". Without a mind the honest refusal stays.

## 0.7.2

- **The installed runtime is `@sym-bot/sym` — the retired engine is gone.** XMesh runs on the
  open MMP 2.0 implementation directly rather than a second engine lineage that could drift
  from it. 0.7.1 declared the retired `@sym-bot/core` as a dependency, so an install pulled it
  even though nothing required it any more; this release is what makes that true for everyone
  rather than only for a deployment built from source. A release gate now refuses to publish an
  artifact that declares or imports it, so it cannot return quietly.
- **Relevance is semantic.** The whole relevance path now encodes meaning instead of comparing
  literal word overlap, which by construction let a paraphrase lose to a record that merely
  shared trigrams. Measured on a definitional question: the definition-bearing record ranks
  first at 0.706, where chatter used to beat it 0.631 to 0.296.
- **Answers keep their shape.** A synthesis is delivered in paragraphs. Every answer previously
  arrived as one unbroken block regardless of how it was written, because three separate places
  collapsed newlines while tidying whitespace. Grounding is unchanged: an unresolvable citation
  is still stripped and an uncited sentence is still dropped where the contract requires one.
- **Conversations persist, and rooms remember them.** Every answered ask in a room is recorded
  and replayed when you return, so a follow-up reads as the follow-up it is.
- **Per-room model policy.** A room chooses which model answers, which does the work, and which
  critiques it — a layer above the model seam, never a bypass: admission and the critic gate
  still stand between a worker's output and you.
- **Declared authoritative sites.** A deployment can name the references it treats as primary,
  and the answering model consults and cites them ahead of generic web coverage on the
  organisation's own vocabulary.
- **Mission outputs.** `GET /api/commissions/:key/outputs` lists every file a mission produced —
  not just the first one its record happened to name — with size, when it landed, and whether
  it is new or a change to something tracked. A companion endpoint returns a file's diff.
- **Automations — recurring missions.** Say once that something should recur and the mesh
  commissions it on an interval, with acceptance criteria checked exactly as a one-off mission's
  are. A run is skipped rather than stacked while its predecessor is still working, the skip is
  recorded, intervals have a floor, and a schedule can carry a hard run cap — it spends real
  work while nobody is watching.
- **Fixed: the admitted count was always zero.** A node's tally separated its own cognition from
  what it admitted by authorship, but admitting a peer's record produces the receiver's own
  remix — so the test could never be true and every node reported nothing admitted while holding
  thousands. Counted correctly, a representative node reports 602 authored and 822 admitted.
- `Authorization: Bearer` is accepted on every endpoint; the query-string token survives only
  for event streams, which cannot set headers.

## 0.7.1

- **Licence v2 — per production team, enforced at the operator (D9).** The licence unit is the
  production team root: a stable teamId derived from the canonical `XMESH_TEAM_ROOT` path (a
  copy to another path is refused — copy-refusal by construction). A v2 licence names the
  teamIds it authorises; verification is offline ed25519, no phone-home, no metering, and
  unlimited users and agents inside a licensed team. Existing v1 seat licences still verify and
  keep their seats through expiry; v2 never caps user creation.
- **Enforcement is the operator, not a meter.** Starting a room operator — the persistent node
  a room needs to persist cognition — checks the team-root licence. Licensed → start. The first
  unlicensed deployment on a host → evaluation (watermarked), so a sandbox still works fully. A
  second unlicensed deployment on the same host → refused: running several production team roots
  on one host to serve several teams needs a licence each. Operator identities are now
  teamId-scoped, so two licensed team roots on one host no longer collide.
- `verifyLicense` trusts a set of signing keys, so the offline licence authority can add a key
  without invalidating anything already issued.

## 0.7.0

- **Every install learns.** The grounded learning subsystem — reinforcement, convergence,
  next-move bias, adaptation, and the validation economy — now ships in the free artifact
  too. There is no capability difference between free and licensed deployments; the licence
  covers production use per team, never features.
- **The learning code stays closed.** It moves to `@sym-bot/xmesh-core` and arrives as an
  opaque compiled bundle. The release gates enforce both halves: no learning source in this
  tarball, and the closed-core dependency present so the capability actually arrives.
- The old "BASE tier" startup lines are gone: a missing learning module is now reported as
  a broken install, never a tier.

## 0.6.8

- **The MCP wrapper ships in the public package.** A coding agent can now drive the
  Developer Runtime through MCP with a one-line config: command `xmesh-mcp`. Six tools —
  mesh_ask, mesh_mission, mesh_board, mesh_missions, mesh_recall, mesh_canon — the same
  surface SYM.BOT's own agent seats use. Previously the public artifact carried the HTTP
  API only.

## 0.6.7

- **`xmesh --version` prints the version and exits.** Previously any unrecognized argument —
  including a probe for a version flag — fell through to the default arm and started a full
  server. `-v` and `version` work too, and `--help` documents it.
- **The MCP bridge finds its passcode under a team root.** `xmesh-mcp-server.mjs` now resolves
  the passcode through `XMESH_STATE_DIR` / `XMESH_TEAM_ROOT` with the same precedence as the
  server itself. A bridge that only knew `~/.xmesh` authenticated against the wrong tenant's
  passcode file the moment the runtime moved to a team root — a 403 with both sides healthy.

## 0.6.6

- **Review missions observe the room they review.** A commissioned reviewer is now peered
  into the room whose work it evaluates, instead of only the lane where its score is
  recorded. Reviews are grounded in the observed record rather than the submitted summary.
- **Product surfaces say exactly what ships.** The README, the npm package description, and
  the runtime's landing page were aligned with the shipped artifact: Developer Runtime is
  API-only, Station is not in the npm package, and capabilities that ship with the paid
  Team Mesh deployment are no longer implied for the public install. Regression contracts
  now pin these statements.
- The runtime serves an accurate landing page at `/` describing the API surface; all
  `/api/*` paths remain unchanged.

## 0.6.5

- **The published artifact is an artifact, not an annotated codebase.** 0.6.4's dist carried
  3,093 comment lines — every design-rationale header readable on npm's code tab. The build
  now strips comments and minifies per-file (structure preserved; bin and worker entry paths
  unchanged). No behaviour change; the working tree keeps its commentary.

## 0.6.4

- **One vocabulary in the internals: `field` becomes `category`** across server, worker and
  tests — with the boundaries kept: the neural sidecar's `field_drifts` key, the wire-contract
  fixture (whose subject is the legacy container), and the customer-facing "Field owner" in the
  blocker taxonomy, which names a data-field owner and is another domain's word.
- Pins move to the renamed stack: `@sym-bot/core` 0.10.0, `@sym-bot/sym` 0.11.2 — verified by
  the full suite including the cross-version wire fixture against the installed artifacts.
- Blockers are judged for coherence BEFORE truncation (PR #28), so a long real blocker can no
  longer be cut into something the register rejects as a fragment.

## 0.6.3

- **Preserved rooms carry generic `x-` names** (`x-review`, `x-research`, `x-ops`). A preserved
  room is a standing cognition node: it outlives every crew, and its name is part of the org's
  ontology — the capability, never the project that first exercised it. Enforced at the
  preserve gate and at `/api/agents` when deploying into a room that does not exist yet (which
  mints a standing node); integrating an agent into any existing room is unchanged. Refusals
  are 400s naming the remedy. Default review room renamed `sym-review` → `x-review`
  (`XMESH_REVIEW_ROOM` still overrides); ops room `xmesh-ops` → `x-ops`.

## 0.6.2

- **`XMESH_WEB_DIST` — a deployment can serve a locally built cockpit while running the pinned
  published artifact.** The package still ships no web build (the 2026-08-07 ruling stands, and a
  test now pins `web/` out of the files list); this is deployment configuration, not packaging.
  A set-but-wrong path announces itself at startup instead of silently serving API-only.

- Blocker CMBs now carry their disposition as a field (`disposition=request|finding|peer-ask|
  abandoned`, marked `(inferred)` when the doer did not declare one), so a report and a request
  are structurally distinguishable. The prefix contract is unchanged; consumers keep working.

## 0.6.1

- **`XMESH_LLM_PROVIDER` is now actually consulted.** 0.6.0 shipped the provider modules and
  nothing called them: declaring a provider had no effect, and the mind was still chosen by the
  legacy variable or by detection. The 0.6.0 notes below describe the declaration as though it
  were live — it was not, and this release is what makes them true. A declaration now decides
  the worker's mind and routes completions, including through the `api:*` lanes, which the
  previous default could not reach at all. Undeclared deployments behave exactly as before.

- Order of precedence, unchanged in spirit and now real: an explicit per-worker mind wins, then
  `XMESH_LLM_PROVIDER`, then the legacy `XMESH_MIND`, then a detected CLI, then nothing — and
  nothing means the spawn is refused rather than silently downgraded.

## 0.6.0

- **CORRECTION (added 0.6.1): the provider declaration described below did not work in 0.6.0.**
  The modules shipped, the settings did nothing, and the refusal behaviour came from
  `XMESH_MIND` rather than from the declaration. 0.6.1 wires it in. The entry is left standing
  rather than rewritten, because a published version's notes should record what was claimed as
  well as what was true.

- **XMesh can now be told what to think with, and refuses to pretend.** A deployment declares
  its provider (`XMESH_LLM_PROVIDER=cli:claude | cli:codex | api:anthropic | api:openai | echo`).
  Unset is not a value: the server starts, serves read-only endpoints and refuses to spawn a
  worker rather than choosing for you. Previously the mind was picked by checking whether the
  `claude` CLI happened to be on `PATH`, and anywhere it was absent — a container, a server, a
  CI box — every worker silently became an `echo` mind. Missions were accepted, workers spawned,
  every endpoint answered 200, and commissions COMPLETED WITH ARTIFACTS, having thought nothing.
  `echo` is still available and now requires asking for it by name.

- **Two ways to supply a model, both first-class.** `cli:*` spawns your signed-in CLI, so a
  personal subscription is used through the session that owns it and no credential is ever
  handed to XMesh. `api:*` is an in-process HTTPS client for deployments that need a key they
  can rotate, scope and attribute to a team. A container or a pod must use `api:*` — a personal
  subscription cannot be shared by a service.

- **Your network stays your network.** Set `XMESH_LLM_BASE_URL` to your own gateway and every
  request is pinned to that origin: a redirect to a vendor host is refused before a socket is
  opened, on retries and failovers alike. Keys are read per call, so `XMESH_LLM_KEY_FILE` picks
  up a rotated secret without a restart — preferred over passing a key in the environment, where
  it is visible to anything that can read the process list.

- **`XMESH_TEAM_ROOT` — one setting names a tenant.** Both state trees hang beneath it
  (`<root>/xmesh`, `<root>/sym`), so a deployment is one directory to back up, move or delete.
  Seventeen per-path overrides already resolved through those two roots and now inherit the
  tenant boundary without being listed individually. Unset changes nothing: existing installs
  keep `~/.xmesh` and `~/.sym`, and an explicit root still wins.

- **`/api/health` can now be false.** It returned the constant `{"ok":true}`, which could not
  distinguish a working deployment from one with an empty canvas and nothing to think with. It
  now reports `ready` as well — a live mesh AND a usable provider — and the authenticated
  `/api/readiness` names which half is missing and what to set.

- **Container and Kubernetes deployment.** A `Dockerfile` that installs a pinned published
  version rather than a working tree, and a manifest at `deploy/kubernetes/xmesh.yaml`: one
  tenant per namespace, one volume, credentials from a Secret, non-root, read-only root
  filesystem. See the README for the two things a container forces you to decide.

- Failures now say which kind they are: a timeout is no longer indistinguishable from a crash,
  and a misconfiguration is never retried against another provider. An exhausted API balance
  reports "add credits" instead of "unrecognised error".

## 0.5.1

- **Pinned to `@sym-bot/core` 0.9.4 — this release exists to make an installed XMesh able to
  exchange CMBs with the rest of the mesh again.** 0.5.0 shipped against core 0.8.1, which
  predates the MMP v1.1 §8.2 rename of the CMB wire container from `fields` to `categories`. A
  0.5.0 install therefore could not exchange a single block with any current node, in either
  direction. XMesh's own code was never at fault — it already reads `categories` throughout; the
  break was entirely the pinned dependency.

  **Upgrading from 0.5.0 is required, and it is not graceful.** The incompatibility is mutual and
  it surfaces as an exception, not a refusal: a node on either side of the boundary raises
  `TypeError: Cannot read properties of undefined (reading 'focus')` out of the receive path when
  handed the other's block. There is no fallback, no warning and no degraded mode. If you are on
  0.5.0, upgrade every node before expecting any of them to talk.

  Verified against the packed artifact rather than the source tree: `npm pack`, install into an
  empty directory, resolve `@sym-bot/core` **from the install** (0.9.4), then push a current
  peer's CMB through `processHeuristicSVAF` — accepted, `aligned`, stored. The same harness
  confirms a 0.5.0-shaped block still throws, which is the upgrade requirement above, measured.

## 0.4.3

- **Pinned to `@sym-bot/core` 0.7.0 and `@sym-bot/sym` 0.10.1.** The shadow redundancy gate now
  records what it computed: one row per admission under `~/.sym/shadow/<node>.jsonl`, carrying
  the receiving node and per-field values. Previously it was reduced to counts in a log line and
  discarded. Admission behaviour is unchanged — the gate still decides nothing.

## 0.4.2

- **Fixed: the licence unit.** The org tier is licensed **per team**, and **priced by the size of
  that team** — the README and the paywall messages said "per organization", which contradicted
  the ruling. The sentence a blocked operator reads at the moment they are asked to pay is the
  one that was wrong, and no test asserted it.
- **Verify what you installed.** The README now documents how to check the packaged interface
  against `web/dist/BUILD-PROVENANCE.json` with any checksum tool — the stamp shipped in 0.4.1,
  but nothing told you how to read it.
- **Pinned to `@sym-bot/core` 0.6.0 and `@sym-bot/sym` 0.10.0.** Brings design C's redundancy
  gate into the receive path **in shadow**: it computes on every admission, is logged, and
  decides nothing. Admission behaviour is unchanged.

## 0.4.1

The control-plane interface ships current, and the package can now prove which build it carries.

- **Fixed: 0.4.0 shipped an older interface than the release it came from.** Every packaging
  check passed — the build was present, non-empty and valid — but it was a snapshot taken days
  earlier, from work that had not landed. Nothing failed, so nothing said so. The interface in
  this release is built from the commit it names.
- **The packaged interface now names its own build.** Each tarball carries
  `web/dist/BUILD-PROVENANCE.json`: the commit the interface was built from, the branch that
  commit had reached, and a SHA-256 for every file it ships. You can recompute those hashes with
  any checksum tool and confirm the interface you installed is the one the release claims.
- **And packaging refuses a build nobody chose.** It stops if the interface was built from
  uncommitted changes, if the build is older than its own sources, or if it comes from a commit
  that never reached the default branch. Each of those ships something no one can point at
  afterwards, and each of them used to pass quietly.

## 0.4.0

The boundary release. XMesh now consumes the v2 two-section record model.

- **Pinned to `@sym-bot/core` 0.5.0 and `@sym-bot/sym` 0.9.0.** A CMB is now two sections —
  `fields` (what the agent says) and `metadata` (what the mesh proves) — with per-field keys.
  Addresses are content-only, so identical content resolves to one address on every node
  instead of forking per node. Authorship is the agent id and is signature-bound, and
  signatures verify against the author's key rather than the delivering peer's.
- **Every consumer read migrated.** Authorship is resolved from the signed author field; an
  unattributed block is skipped rather than published under an invented name.
- **Pre-boundary history stays readable.** Older blocks are carried as unverified-legacy
  rather than rejected.
- **Fixed: the published tarball shipped no web UI.** `web/dist` is a symlink, and npm drops
  symlinks from a tarball silently — `npm pack` exited 0 while the archive contained none of
  the interface. The build now materialises it for the pack and restores the link afterwards,
  and refuses outright if the target is missing or empty.

## 0.3.0 — 2026-07-15 — the group is a cognition node

Missions route through standing-group membranes: the mesh's preserved experts get the work
their memory grounds, and a fresh crew is minted only when no one does. Every seam this
surfaced in production was closed the same day, each pinned by a regression test.

### Added
- **Group-membrane mission routing** — a new mission is offered at every standing group's
  membrane before a group is minted: each preserved node evaluates the task against its own
  store (receiver-autonomous selfSelect; no assigner); the strongest grounding hosts, earned
  authority breaks ties; universal refusal mints the true 0→1. `hostedBy` persists on the
  world as audit provenance; the cockpit card shows "hosted by <group>".
- **Three-dimension membrane gate** — the offer matches on focus, intent, and issue
  semantically and filters the rest; admission is by identification (noise floor + margin
  over every other group), with both live calibration datapoints pinned as tests.
- **Membrane liveness backstop** — an admitted-but-unclaimed mission is staffed with one
  fallback doer in the SAME standing group past a 120s horizon (below the supervisor's
  180s unstaffed pause, which preempted a later rescue).
- **Teach-the-mesh over Abandon** — abandoning a stuck mission offers the missing-skill
  dialog first: a named skill closes the stuck commission and re-runs it in place with a
  provisioned specialist; "just abandon" proceeds; clicking away cancels.
- **xmesh MCP server hardened** — 401/403 re-auth (a daemon restart no longer strands a
  long-lived MCP client), request timeouts, end-to-end stdio tests against a mock API;
  registered in `.mcp.json` so provisioned agents get `mesh_ask`/`mesh_mission`/
  `mesh_recall`/`mesh_canon` out of the box.

### Fixed
- **World teardown resolves its needs-human commissions** — the mission card follows the
  commissions, so a torn-down world with a blocked commission stayed on screen forever
  ("Abandon does nothing").
- **Mission-minted groups retire with their world** — the group's operator node ran until
  process shutdown, leaving every closed mission as a permanent 1/1 ghost group in the
  pickers. Standing groups are never touched; release is eviction, not deletion.
- **Group views are group-pure** — a selected group's mission list rendered every group's
  needs-you commissions; foreign needs-you is now a header count, never cards, and the
  global view keeps rendering everything.
- **Worker teardown kills the whole process tree** — stopping a worker killed only its
  npm-exec wrapper, orphaning the LLM mind, which could finish dead work and revive a
  dismissed commission under a crew that no longer existed.
- **CHECK targets end at a dash** — `CHECK: file review.md — one verdict per …` no longer
  checks for a file named after the whole sentence (a false 0/1 on real, filed work).

## 0.2.2 — 2026-07-12 — the learning half

The validation economy becomes real, verifiable, and center-free. Driven by the mesh's own
audit of its learning implementation; the two hardest design forks were decided by the mesh
advising on its own mechanism.

### Added
- **Center-free authority ledger** — earned authority is now *derived*, never stored: every
  grounded outcome is an ed25519-signed, hash-chained attestation in the issuer's own
  append-only ledger. Tampering breaks the chain and the doctored suffix stops counting.
- **SVAF membrane adaptation** — a node's per-field admission weights learn receiver-locally
  from grounded outcomes (symmetric rule, ratified by the mesh's mechanism advisory: a
  60%-reliable field stays open; only worse-than-chance closes).
- **Wall-clock authority decay** — trust that isn't re-proven fades toward unknown, on both
  the fold and at read (90-day half-life).
- **Sybil / co-objection costs** — anti-self-scoring + asymmetric co-objection sharing:
  sockpuppet farming is non-positive EV; a lone correct dissent still earns full.
- **Cross-host attestation gossip** — a peer's signed ledger folds into any host's view
  after full verification; two hosts provably converge; tampered logs rejected whole.
- **Admission-gated issuer trust** — a foreign issuer contributes only if locally admitted,
  then bounded by a per-subject evidence budget (mesh advisory C+D).
- **Fail-visible learning tier** — `GET /api/learning`, a cockpit `◆ learning / ◇ base`
  chip; a hosted build silently on BASE is now observable.
- Cockpit: per-agent shard rows, live-cognition idle panel, Fleet as an org chart, the
  verdict console (Validate · Apply · Dismiss-and-reskill · Preserve), volunteering.

### Changed
- Deps pinned to the current published engines: `@sym-bot/sym` `^0.7.30`, `@sym-bot/core` `^0.3.48`.

## 0.2.1 — 2026-07-11
- Verdict-console fixes and cockpit hygiene atop the org-tier license gate.

## 0.2.0 — 2026-07-11
- **Org-tier license gate** — offline-verifiable ed25519 license; solo mode free (one
  operator, full local product); org surfaces (second operator, cross-org gateway) licensed.

## 0.1.1 – 0.1.8 — 2026-07-10/11
- First Mission (cognition-node discovery → review → one-click deploy), guided composer,
  CAT7 membranes, dogfood bug fixes; the XMesh mission leads the package.

## 0.1.0 — 2026-07-10
- First public release: `npm i -g @sym-bot/xmesh` → the server + bundled cockpit. Closed
  dist; the learning spine excluded (BASE tier).
