# 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.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).
