# `/api/v1/mission`: the one API of an xmesh runtime

**Status: shipped in xmesh 0.10.0.** The model is `src/mission-v1.ts`, and the published JSON Schema `schemas/v1/mission.schema.json` is generated from it. `test/mission-v1-schema.test.ts` checks four things:
- the committed schema is exactly what the model generates
- every `$ref` resolves
- a real request of every kind is accepted
- malformed requests are refused

Every scenario the engine ships (15 files at xmesh-grid-web `694513f`) passes the scenario shape.

**Founder direction, 27 September 2026, verbatim:**
- "one api uri"
- "we event should use api/v1/mission only" / "not missions, and add v1"
- "this should be also the xmesh simulation mission post call"
- "only one mission allowed"
- "need a sound and complete json post and response schema/model"
- "clear all other legacy method, code comments to avoid any confusion. It's safe to do now as no any xmesh runtime user so far except us"
- "api design needs codex-win review"

**Scope answers the founder chose:** `/api/v1/mission` is the only public API. Recall, canon, board and approvals become mission operations or views. Sign-in, health and the cockpit's live-event stream stay as internal plumbing. MCP gets one tool (`mesh_mission`) and the CLI one verb (`xmesh mission`).

## 1. Paths and methods

| Method | Path | Does | Body | Success |
|---|---|---|---|---|
| POST | `/api/v1/mission` | hand the mesh a mission of any kind: **one request, one mission** | `MissionRequest` | `201` created · `202` created and waiting on capacity · `200` only for a retry of the same request under the same `Idempotency-Key` (§5) |
| GET | `/api/v1/mission` | read the collection; `view=` selects list (default), `capabilities`, `board`, `register`, `recall` or `canon` | — | `MissionPage` · `Capabilities` · `Board` · `Register` · `Recall` · `Canon` |
| GET | `/api/v1/mission/{id}` | read one mission; `part=` selects `outputs`, `diff`, `log`, `artifact`, `root-cause`, `square` or `sym`; `Accept: text/event-stream` opens its live feed | — | `Mission` · `Part` · `FeedEvent` stream |
| PATCH | `/api/v1/mission/{id}` | operate on a mission | `MissionOperation` | `200` `OperationResponse {mission, child}`: `child` is the mission an op created (replay, apply, reskill, take-move, root-cause, start), else null |
| DELETE | `/api/v1/mission/{id}` | abandon it (§6) | — | `200` `Mission` |

Nothing else is public. There is no plural, no second mission endpoint and no per-kind door. The mission id is `mission-<hex>` for every kind.

## 2. One request shape: CAT7 plus a typed `spec`

```json
{ "kind": "work" | "question" | "review" | "research" | "assessment" | "task" | "simulation",
  "cat7": { "focus": "…required…", "issue": "…", "intent": "…", "motivation": "…",
            "commitment": "…", "perspective": "…", "mood": { "text": "…", "valence": 0, "arousal": 0 } },
  "spec": { "…typed per kind…" } }
```

- **CAT7 carries meaning.** The runtime emits the request as a CMB: receivers admit it through SVAF, store it with lineage, and recall it by meaning. `Mission.cmb` is its address. For work-shaped kinds, `commitment` holds the acceptance criteria. Its `CHECK:` clauses are the harness grammar (`file`, `run`, `contains`, `not-contains`, `contains-at-least`), and any other `CHECK:` is refused.
- **`spec` carries what must be validated exactly:** rooms, paths and seeds. Every object this API defines is closed, so an unknown key is refused.
- **The scenario and the gear are the engine's.** The generated schema types `Scenario` as an object and names no engine version: the simulation host names the schema it validates against (`GET /v1/host` `engine.features.scenarioSchema`), and `capabilities` relays it as `scenarioSchema:<id>`. A pinned `$ref` to `…/scenario/1` went stale when the engine reached 4. The simulation host validates both with `validateScenario(sc, {closed: true})`, which never throws and refuses unknown fields; its reasons become `422` details. So there is one engine and one validator. A host that names no `scenarioSchema` predates this, and a client cannot know which fields it accepts: send it only what that host's own documentation names, and read a `422`'s details as the answer, not as an error to retry.
- **The schema's `$id` names the v1 contract, not a release.** It stays the same while v1 changes only additively (new optional fields, new kinds, new enum values), and the file's `$comment` says so. The release that generated a copy is the runtime's version (`GET /api/v1/_/version`). A breaking change gets a new path, never a reused `$id`.
- **The kind is always explicit.** It is never inferred from wording or from the caller. Who asked is `requestedBy`, taken from the session and never from the body: `{type: "agent", id: <seat>}` for a seat's session, `{type: "user", id}` for a person.
- **What goes out on the mesh, per kind:**
  - `work`, `review`, `research` and `assessment` are **dispatched**: the request goes out as a room request that workers pick up by its CAT7 `intent`. So for these kinds `cat7.intent` is left out or is `"request"`, and any other value is refused (`422` on `cat7.intent`), rather than emitted as a lie or never picked up.
  - The request CMB carries the mission id. That is what keeps one request one mission on the mesh: two identical asks have different content addresses, so neither can be answered with the other's commission.
  - `question`: the question goes out as the ask's own question CMB (`cat7.focus` is the question). The other CAT7 fields are kept with the mission.
  - `task` and `simulation` emit nothing in v1, so `Mission.cmb` is `null`. A task's `start` creates a `work` child, which has its own.
- **Signature binding (open, §14.4):** CAT7 is signed in the CMB; `spec` is **not**, in v1.
  - The runtime validates the spec, stores it, and records `specDigest`: the SHA-256 of its canonical JSON (sorted keys, no insignificant whitespace), taken at acceptance.
  - Every evidence record the runtime writes for the mission references that digest.
  - A spec never inherits the CAT7 signature by sitting next to it.
  - Binding the spec into the requester's signature needs MMP v2.0's signed `metadata.application`, which sym 0.13.9 keeps switched off (`MMP_EMIT_V2 = false`).

| Kind | What it is | Executor | Replaces |
|---|---|---|---|
| `work` | real work by the mesh's minds, with real effects (files, repositories) | minds | `POST /api/worlds`, `POST /api/commissions` |
| `question` | the mesh answers, each claim cited to the agent that made it | answer mind plus contributors | `GET /api/ask` (SSE) |
| `review` | a second opinion on a file or saved diff, with the critic's grounds fixed | minds | `POST /api/recipes/review` |
| `research` | a sourced research document | minds | `xmesh research` (which posts to the retired `/api/worlds` and is broken since 0.9.39) |
| `assessment` | the integration-assessment recipe | minds | `POST /api/recipes/integration-assessment` |
| `task` | a board card that people and agents carry; no mind is dispatched until `start` | people | `/api/backlog*` |
| `simulation` | a world run on the grid engine; nothing outside the simulated world changes | engine | new (the same POST, per the founder) |

A task's `result` is its card: `column`, `owner`, `size`, `rank`, `reviews`, `actions`, and `origin`. `origin {commission, room, artifact}` names the finding a card was made from, so a card shows its source; it is `null` for a card a person or an agent filed. The runtime sets it; a request cannot (it is part of the result, not the spec). Added in 0.10.3.

A review's doer reads git through one read-only MCP tool, `git` on the `xmesh-git` server, offered to a review's claude doer and a review panel's claude lens, and to no other run. It runs `git show`, `git log`, `git diff` and `git ls-tree` only, on the commits of the run's own directories' repositories and the git repository of the file under review (for a lens, of the proof artifact): git never reads a work tree or an index, so `git diff` compares commits. Every argument is decided before git starts: an option is one the tool lists for that command, given as `--name=value` when it takes a value; every other argument before `--` is a revision, resolved to an object id before git sees it; paths follow `--`, relative and never starting with `-`; any other git command is refused. git reads the repository the way the runtime does: in a private repository of the runtime's over the repository's objects, with every config file switched off, under the operating system's sandbox, so the repository's own config (hooks, external diff, textconv and filters, pager, signature programs, the help viewer, submodules) runs nothing and no index is written; on a host with no enforcing sandbox the tool answers "not stated". The brief tells the doer to take every fact it cites from a commit from this tool. Added in 0.11.0.

## 3. Lifecycle

`status` is one of `accepted`, `running`, `waiting`, `blocked`, `completed`, `failed` or `abandoned`. A human verdict is a separate field, `verdict: validated | dismissed`, and never a status. Abandoning is not a verdict: an abandoned mission carries the verdict a person gave it, or none.

| From | To | When |
|---|---|---|
| accepted | running | a mind accepts · the synthesis starts · the engine starts · a task goes in-progress |
| accepted | waiting | `on: capacity` (the governor queue; `position` and `cap` given) or `on: budget` (a validator must approve spend) |
| running | waiting | `on: approval` (a capability request is pending; `requests` names it) · `on: person` (a supervisor pause) · `on: review` (a task in review) |
| waiting | running | the capacity frees, the decision lands, or `resume` |
| running | blocked | the worker stopped with a reason. A finding the work routed around does not block; it is recorded in `blocked.findings` or on the evidence. |
| blocked or waiting | running | `resume` |
| running | completed | a result exists. For work, the agent reported completion and the harness ran the checks. |
| running | failed | a terminal failure with nothing delivered: an engine error, an unrecoverable mind error, or a deadline overrun with no deliverable |
| any non-terminal | abandoned | `DELETE` |

- Terminal states are `completed`, `failed` and `abandoned`.
- A mission the runtime accepted and then could not carry out reads `failed`, and `fault` says why (for example the spawn error, or a spend refusal that arrived after acceptance). A refusal that comes before a mission exists is an error response, and no mission is created.
- `replay`, `apply`, `reskill`, `take-move`, `root-cause` and `start` never mutate the mission they act on. Each creates a child, recorded in `lineage` (`parent`, `relation`, `children`).
- **How today's records map** (for implementation):
  - Commission statuses: `requested` → accepted; `accepted` → running; `paused` → waiting (person or approval); `blocked` → blocked; `completed`, `validated` and `dismissed` → completed plus `verdict`.
  - World statuses: `unassembled` → waiting on capacity (or failed once non-retryable); `thinking` → running; `complete` → completed; `torn-down` → by outcome.

## 4. Operations: `PATCH /api/v1/mission/{id}`

| op | Kinds | Precondition | Who | Replaces |
|---|---|---|---|---|
| `verdict` | work-shaped | status completed; or blocked with findings; or waiting on approval with `permissionStop` | validator+ | `POST /api/lifecycle/:key` |
| `decide` | any | the named capability request is pending | the request's `minRole`, counting toward its `quorum`; one deny is terminal | `POST /api/approvals/:key` |
| `resume` | work-shaped | waiting or blocked | participant+ | `/resume` |
| `replay` | work-shaped | completed, blocked or failed; creates a child | participant+ | `/replay` |
| `apply` | work-shaped | an artifact exists; creates a child | participant+ | `/apply` |
| `reskill` | work-shaped | creates a child with a specialist | participant+ | `/reskill` |
| `propose-next` | work-shaped | completed | participant+ | `/next-moves` |
| `take-move` | work-shaped | proposed moves exist; creates a child | participant+ | `/moves/:idx/accept` |
| `root-cause` | work-shaped | an objection, an end-of-day break, or blocked; creates a deep child. When the child completes, its report's diagnosis is recorded on this mission as `diagnosis` (§9). The shallow read is `part=root-cause`. | participant+ | `/rca`, `/rca/deep` |
| `note` | any | none; it changes nothing about the run. On a task it is a card action (`result.actions`); on any other kind it is kept on the mission as `notes` (§9) | participant+ | `/api/backlog/:key/note` |
| `claim`, `move`, `size`, `review`, `rank`, `correct`, `link` | task | the board's legal-move table; `blocked` needs a reason; `done` needs an approve review or the owner's reviewed-none | participant+ (`correct`: validator+) | `/api/backlog/:key/*` |
| `start` | task | column is ready; creates a `work` child | validator+ | `/api/backlog/:key/start` |

An illegal transition returns `409 invalid_transition`, naming the current state and the legal ops.

- **One change at a time.** The PATCH ops and `DELETE` on one mission run one at a time, each on the mission as the previous one left it. Two `start`s of one card make one child; the second gets `409 invalid_transition`, because the card is no longer ready.
- **Spend first.** The ops that commission new work from the board or a finding (`start`, `root-cause`) read the spend contract before anything is created, as `POST` does. A spent day answers `402 budget_exceeded`, whose `hint` names where a validator changes the cap.

## 5. Idempotency and retries

- **`Idempotency-Key` header** (1–200 printable characters). It applies to POST, and to the PATCH ops that create a child (`replay`, `apply`, `reskill`, `take-move`, `root-cause`, `start`).
- **What a key is bound to** (codex-win F1): the tenant, the principal, the method, the **canonical target** (`POST /api/v1/mission`, or `PATCH /api/v1/mission/{id}` with that exact id), and the canonical-JSON hash of the body.
  - The same key used with any other method, target or body is `409 idempotency_conflict`.
  - So the same key and `{op: "replay"}` sent to missions A and B can never return A's child for B.
- **Reservation is atomic.** The key is reserved, as *in progress*, before anything executes.
  - A concurrent retry while the first is still running gets `409 idempotency_in_progress`, with `retryable: true` and `retryAfterS`.
  - Once the mission (or child) exists, its id is written against the key in the same step.
- **After a crash:** a retry after a crash that *committed* the mission returns that mission. A reservation with no committed mission, left behind by a crash before anything was created, expires after 60 s, and a retry then executes afresh.
- **Storage:** the record is kept for 24 hours.
- **Tests owed with the implementation:** concurrent retries, a crash before the response, and one key across two targets.
- **Behaviour:**
  - The same key, target and body return that mission (or `OperationResponse`), with `200`.
  - Anything else under the same key is a `409`, never silently reused work.
  - With no key, every POST creates a new mission.
- **One request, one mission** (founder, 27 September, verbatim: "One request one mission").
  - Every POST creates exactly one new mission, with its own id, `requestedBy`, `specDigest`, lifecycle and evidence.
  - No request is ever answered with another request's mission, and no two requests are merged into one.
  - Today's implicit behaviours are **removed**: the open-duplicate reuse in admission, and the JOIN that filed a new ask into a live world.
  - Execution may still place two missions' work in the same room. Their identities, operations and evidence stay separate.
  - The one exception is not an exception: a retry carrying the same `Idempotency-Key` and a byte-identical canonical body is the *same* request, and gets back the mission it already created.
- **Retries:** a `waiting` `202` is not an error, so don't retry it; poll `GET /api/v1/mission/{id}`. `429` and `503` carry `retryable: true`, and `429` also carries `retryAfterS`.

## 6. Cancellation: `DELETE /api/v1/mission/{id}`

The call is idempotent: abandoning an abandoned mission returns it again. A completed or failed mission cannot be abandoned (`409 invalid_transition`). The abandonment is recorded before anything is stopped, so work that was still being set up when the call arrived (a world waiting behind the mind governor, say) is stopped as soon as it exists. If stopping fails, the mission still reads `abandoned`, and its `fault` says what could not be stopped. What it stops depends on the kind:

| Kind | Effect |
|---|---|
| work, review, research, assessment | A world of the mission's own is torn down: its workers stop, its open commissions are dismissed, and a room minted for it is released. A commission filed into a room is dismissed. A dismissed commission refuses a late completion, so nothing books after the abandonment. Open child missions are abandoned with it. Results and evidence already recorded are kept. **Effects already made in the world are not undone** (files written, commits made). |
| question | No further mind call starts. One already running finishes, and its answer is not recorded. Steps already on the feed stay there. |
| task | The card moves to dropped. |
| simulation | An unfinished run reads `stopping` until the host has reaped its worker, then `outcome.status: "stopped"` with `stopped {reason, by, tick}` at the last tick the worker acknowledged; it never reads `running`. A stop that cannot complete stays `stopping`, and the mission's `fault` says so. The runtime keeps reading the host after the abandonment and updates the result until every run has settled: the first snapshot is not the record. Abandoning again while runs are stopping is safe and returns the same state. Finished runs keep their result and final fingerprint. A stopped run never passes as a finished one. |

Nothing is deleted. Missions and their evidence have no age limit (founder ruling, 26 September).

## 7. Access

- **Authentication:** `Authorization: Bearer <token>` or `x-mesh-token`. A token in the query string is accepted **only** on a `text/event-stream` request, because `EventSource` cannot set headers. Clients that can should send the header.
- **Tenant:** every mission belongs to its deployment's tenant, and nothing crosses tenants.
- **Visibility:**
  - A mission requested by an agent is visible to the whole tenant (as today).
  - A simulation requested by a user is private by default: the requester, plus validators and anchors.
  - `spec.visibility` overrides either default.
- **Roles, for creating:**
  - `review` and `assessment` need validator+ (as today).
  - `overBudget` (on any kind) and the work field `project` need validator+. Below that, the request is refused with `403`; it is never quietly run without them.
  - Every other kind needs participant+.
- **Simulation observation boundary** (codex-win F3). Without an x-ray grant, everything a viewer receives is computed **server-side** from its own mesh's picture (what its syms perceived and admitted), never from the truth:
  - `counts`, `acts`, `sides` and `byCell` cover the viewer's own mesh only. Other meshes appear only as that mesh's picture holds them.
  - The `front` is computed from the picture.
  - Listed events are those its mesh took part in or perceived.
  - `dropped` counts only authorized events, so a total can never reveal what the viewer may not see.
  - `part=square` returns the picture. `part=sym` returns only the viewer mesh's own syms.
  - Full-world counts, positions and cards are **truth**, served only on an x-ray view granted to the requester or validator+, and marked `basis: "truth"` in every frame.
  - **No shared counter reaches a picture view.** A number drawn from a counter every mesh advances leaks the rival's hidden activity even when every gate on content holds. The engine's word `seq` and event `seq` are both one counter per run or world.
    - So a picture view never samples, orders or identifies by one: words are sampled by the per-word key (§10), listed events leave out `seq`, and `diagnosticDigest` is null.
    - Ids minted from a world-wide counter are the host's to check against this rule before serving them; `w.objN` in ruin and lake ids is one, and whether a mesh's actions can mint it mid-run is not yet checked.
  - **Proof and results are audit data** (codex-win R3). Checkpoints, fingerprints, replay receipts and the metrics in a simulation's result digest the whole world. They go to the principals an x-ray could be granted to (the requester, validators and anchors), on the mission and on an x-ray stream, and the same holds on replay. Anyone else who can see the mission reads `result: null` and no receipts; a picture stream never carries a checkpoint.
  - **Tests owed with the implementation:**
    - A hidden rival change never reaches a picture view. That includes a change that only **renumbers**: more rival words or events sent earlier shift every later world-wide number (dev2's probe, m147, is the model: truth off, every output digested, the rival's traffic changed, the view byte-identical).
    - A `Last-Event-ID` from one view cannot replay another view's events.

## 8. Reads, pagination and availability

- **List:**
  - `limit` 1–200 (default 50), plus an opaque `cursor` taken from `next`.
  - Sorted by `updatedAt` descending, then `id`. `updatedAt` moves with everything recorded on the mission: its lifecycle, its progress, a critic's approval or objection, a finding disclosed while it is blocked, and a root cause's diagnosis.
  - Filters: `kind`, `status`, `verdict`, `room`, `requestedBy` (`agent:<id>` or `user:<id>`), `since` (ISO time), `q` (substring of focus) and `cmb`.
  - `cmb=<key>` finds the mission a CMB belongs to: the one whose request CMB (`cmb`) or completion CMB (`evidence.reported.cmb`) is that key. The page holds zero items or one, within what the caller may see.
  - A parameter the list does not know is refused with `422 validation_failed`, which names it. It is never ignored: an ignored filter reads as a list that matched.
  - Each row is a `MissionSummary`. It carries only what is stored, so a page costs no CHECK run and no disk walk: who reported the completion (`reportedBy`), the harness's tally (`checked {passed, total}`), `parent`, the completion's first artifact (`firstArtifact`) and its CMB (`completionCmb`), whether it held up (`grounding`), and the critics' tally (`review {approvals, objections}`, `{0, 0}` while a configured critic has not ruled, `null` when none was configured). The clauses, outputs and the rest are on the mission's own read.
  - What a surface counts across missions, from the rows alone: what a waiting mission waits for (`waitingOn`: `capacity`, `approval`, `budget`, `review` or `person`, `null` unless it is waiting); the block's `blockerType` and `gapLayer` (both `null` unless the mission is blocked); `requests`, the capability requests tallied by shape (`[{id, capability, pending, approved, denied}]`, keyed by `presentation.id`, in the order each shape was first asked, `[]` when none was raised); and `diagnosis {locus, missing}`, the newest root cause's claim (`null` until one has completed). Who claimed it, its evidence and any ruling are on the mission's own read.
- **`view=board`:** every task not dropped, grouped by column, with the legal-move table. Capped at 1,000; `total` and `truncated` say when the cap bites. Each card is a `BoardCard`: the task's summary plus its `owner`, `size`, `rank`, `blockReason` and `lastAction` (its newest action `{by, at, text}`, `null` when it has none). Its actions and reviews in full are on the mission's read.
- **`view=register`:** the integration blocker register, built from the missions' own stops (§13).
- **`view=recall`:** `q`, `category` (a CAT7 field or `all`), `mode` (`match` or `relevance`), and `limit` 1–60 (up to 200 with `deep=1`). With `mode=relevance`, `expand=1` joins each anchor's siblings from this deployment's own mission rooms after the anchors, the same join the mission brief ships, and `expanded` says how many of the hits they are. A partial or failed search says so in `state` and `fault`; an empty list is never presented as an empty memory.
- **`view=canon`:** `limit` 1–500 plus `cursor`, and the response carries `next`.
- **`view=capabilities`:** which kinds this deployment can run and why not, the engine and its features, the lanes (whether a credential is present, never its value), and the limits.
- **Availability:** `result` is null until the mission is terminal, or partial for an abandoned simulation. Large content comes through `part=`: artifact and log up to 100 KB, diff up to 200 KB, each with `truncated`.

## 9. Evidence: three tiers, never merged

| Tier | Field | Means | Never implies |
|---|---|---|---|
| Reported | `evidence.reported` | what the agent **says** it did: summary, artifact locators, and `cmb`, the completion CMB it published (null when none was recorded) | that it happened |
| Checked | `evidence.checked` | what the **harness** checked (not the agent that did the work): each CHECK clause with pass, fail or null | that unchecked prose holds |
| Verified | `evidence.replays` | a simulation run **replayed** on the pinned engine to this exact fingerprint | anything about the runtime's real effects |

**`evidence.review`** is present whenever an independent critic was configured for the mission (a work mission's world records it; a recipe records it on the mission when it starts), and null when none was. Present with no objection and no approval means the critic has not ruled: while the mission runs, it may yet; once the mission has completed, it did not, which is an unreviewed mission, never a pass.

Beside the tiers, apart from them: **`evidence.grounding {verified, failed, lastAt}`**, reality's answer after the ruling. It counts grounding CMBs whose lineage reaches the mission's request or completion. A `verified` counts from any observer. A `failed` counts only when it is own-authored or a person's verdict; an untrusted peer's `failed` is observed and never counted, because claiming something broke is the attack surface. It is null until someone grounds it. It says whether what the mission produced held up, not whether the work ran. The runtime keeps the tally in the background: every 30 seconds it reads only the stored CMBs it has not read before, never all at once on the thread that serves requests, so a mission's read or a list page never walks the stores for it. The first reading after a start begins once the API is listening and takes seconds; until it completes, `grounding` is null for every mission, and `GET /api/v1/_/scheduler` reports `grounding.ready: false`.

**`diagnosis`**: where the mesh's cognition ran out, as the mission's newest deep root cause **claims** it. It is null until a root cause (op `root-cause`) has completed.
- **`locus`** is one of:
  - `knowledge`: the mesh did not know something the step needed.
  - `skill`: the agent could not do something the step needed; this is not a permission.
  - `source`: a source it relied on misled it.
  - `approval`: it needed a person's decision or a grant it did not hold.
  - `none`: there was no material blocker.
  - `undetermined`: the evidence does not decide it.
- **`missing`** names the gap, and **`evidence`** says what the claim stands on. Both are in the root cause's own words.
- **How it is recorded.** Both deep briefs end the report with exactly one fenced `diagnosis` block holding that JSON object. The runtime parses it strictly and records it on the mission the root cause traced. It never repairs a block and never guesses.
- **`reason`** is set when the runtime read the diagnosis as undetermined itself: no block, more than one, one that does not parse or that contradicts itself (a gap that names nothing missing, or `none` that names something), or a report the runtime could not read. A report it could not look at says so, and is never shown as a report with no diagnosis.
- **`source`** names the root-cause mission it came from and the report it was read from.
- **Never implies that anything checked it.** `ruling` is a person's verdict on that root-cause mission. It rules on the report as a whole, and it is the only check v1 records.
- A mission keeps its newest diagnosis, by the root cause's completion. Earlier ones stay on their own root-cause missions.

**`pendingVerification`** (top level, beside the evidence and never part of it; codex-win B-7): lifecycle **claims** no one could verify, `[{key, intent, claimedBy, at}]`, oldest first, one per CMB key, at most 20, and `[]` when there are none.
- **Where a claim comes from.** A commission's lifecycle (accept, progress, complete, blocker, delivery, objection, verdict, disclose) changes only on a verified record: its author's signature checked against the key bound for that author, by the control plane or by a backstop that verifies the author's own stored record. A route that only holds a copy says nothing about who wrote it: the room operator's admitted copy is the operator's own remix, signed by the operator; a lifecycle-inbox file is unsigned; a stored record no bound key verifies (a first contact, a tampered file) proves nothing about its author. What such a route says is kept here instead.
- **`claimedBy`** is who the copy names as its author. It is unverified and credited to no one, and `""` when the copy names nobody.
- **A claim triggers nothing.** It moves no status, credits no one, and raises no approval, grounding or growth.
- **It clears** when a verified record with the same key arrives; that record makes the one transition. A copy of a record already verified is never a claim. A claim moves `updatedAt`, and so does its clearing.

**`notes`** (top level): what people recorded on the mission with op `note`, `[{by, text, at}]`, oldest first, the newest 50, and `[]` when there are none. Optional in the schema, as every field v1 adds is (§2): a runtime from 0.10.11 always sends it, and a Mission an earlier runtime returned has none, which reads as `[]`. A note is a record about the mission, such as a commissioner's "received and used" on a review; it is not a step of the run and moves no status. A task mission's notes are its card's actions (`result.actions`).

**`notices`** (top level, apart from `progress`): the runtime's routine notices about mesh traffic around the mission, `[{kind, by, text, at, count?, lastAt?}]`, oldest first, the newest 20, and `[]` when there are none. Optional in the schema like `notes`: always sent from 0.10.11, absent (read as `[]`) from an earlier runtime. `kind` is `delivery` (a mesh delivery for the mission's commission was refused because its author is not a party to it) or `child-request` (a request to register a child of the commission was refused for the same reason). A repeat of the same notice is counted, not added. A notice is expected traffic: a client shows it as a notice, never as the run's progress or as a fault, and nothing reads it as a sign that anyone worked.

**Binding** (codex-win F2):
- Every evidence record (reported, checked, review, capability request, replay receipt) and every streamed `evidence` event carries `binding {mission, specDigest}`.
- The runtime checks the binding against the mission before it records, streams or exports the record.
- It is the runtime's attribution, not the requester's signature; the spec stays unsigned in v1 (§2).

Alongside these:
- `evidence.review` holds the critics' objections, approvals and lens judgements. Silence from a configured critic is not approval.
- `evidence.requests` holds capability requests with every vote. Each carries the blocked mind's own words (`capability`, kept for the record), `presentation` (the plain-language question a person answers, a recommendation and its one line of why, and `humanDecidable`: false when the capability has to be set up rather than approved) and `grantEffect` (what approving actually does, which can be broader than what was asked for). The runtime derives both, so every client shows the same decision. A mission waiting on approval puts the pending questions in `waiting.detail`.
- **Nothing today captures a change set independently of the agent's report.** Completion from disk reconciliation binds an artifact to a path and a settle window, not to a writer. The schema doesn't pretend otherwise.

## 10. Live feed

`GET /api/v1/mission/{id}` with `Accept: text/event-stream`.

- **Event format:** every event is `{v: "xmesh.feed/1", seq, type, at, data}`, and the SSE `id:` is `seq`.
- **Sequence:** `seq` starts at 1 per mission and only increases.
- **Stream identity** (codex-win F6): a stream is (mission, run = (seed, arm), mesh, x-ray), and `seq` is per stream. A run is identified by its (seed, arm), and a mission's seeds and arms are unique, with `growth` always stated, so each arm has one spelling. The host's own run ids may carry a repeat index for a seed run again (dev3's host plan); v1 exposes none, and a duplicate seed is refused.
- **Resume tokens** (codex-win): the SSE `id:` is an **opaque resume token** that binds the stream identity (mission, run, mesh, x-ray) to the event's `seq`, because a bare integer cannot say which stream it came from. The event body still carries `seq`.
- **Resuming:** resume with `Last-Event-ID` (the token) or `?since=`, and only within the same stream identity. A token from another run, mesh or x-ray setting is refused (`422` on `Last-Event-ID`), never replayed into this stream; connect without it to start that stream. Changing the viewport or rate keeps the stream, and replay is filtered by the resumed request's viewport.
  - `status`, `progress`, `evidence`, `answer-step`, `sim-second` and `end` are durable and replayed on resume (within the authorized view). `sim-checkpoint` is too, on an x-ray stream only (§7).
  - `sim-frame` is ephemeral: a reconnect gets a `gap` for the skipped frames, then the latest frame.
- **Gaps:** a `seq` the host no longer holds gets an explicit `gap {from, to, reason}`, never a silent jump.
- **The feed is a reconnect window, not the record** (codex-win). The runtime holds up to 1,000 events per mission, in memory, for an hour after the mission's end, and a restart starts it again; `capabilities.limits.feed` reports this, with `durable: false`.
  - What survives independently, and is read through `GET /api/v1/mission/{id}`: the mission record (spec and digest, requester, verdict, links, fault, result), the commission's history and evidence, its newest 40 progress entries with a count of all of them (older entries are not kept), a card's actions, and the mesh's CMBs.
  - What cannot be recovered once it ages out, or after a restart: the feed's own event sequence and timing, and a question's intermediate answer steps, which are kept nowhere else.
  - The founder's no-time-limit retention applies to memory and to the mission record. This window is not where that promise is kept.
- **Heartbeat:** every 15 s.
- **Simulation parameters:** `seed`, `arm`, `mesh` (whose picture), `viewport=x0,y0,x1,y1`, `xray=1` (§7) and `rate=1..10` frames per second. The engine steps at 20 ticks a second, so 10 Hz means every second tick.
- **Changing the view:** pan and zoom reconnect with the new parameters and the same token; the host answers with a fresh `sim-join` carrying the view it applied, and `seq` continues. Another mesh, seed or x-ray setting is another stream, opened without the token. SSE is one-way; the view lives in the request, so nothing is held server-side between connections.
- **What a viewer gets:**
  - `sim-join`: the scenario's identity `{id, fingerprint}` (a viewer that draws ground from its own copy refuses a mismatch, and an absent identity refuses rather than defaulting), story, seed, arm, engine, the mesh shown, x-ray, and the view applied.
  - `sim-frame`, up to 10 Hz:
    - `detail` is `crew`, `dots` or `cells`, by the counts in `capabilities.limits`.
    - Viewport syms carry id, x, y, mesh, drained and figure. At crew detail they also carry activity, the step they came from (for interpolation), work and parcel.
    - Per-cell counts cover the area outside the viewport.
    - Squares are shown as the mesh's picture.
    - Words in flight in the viewport (the viewer mesh's own on a picture view, all on x-ray), with `wordsInView` counting every word the sample was drawn from.
      - They are sampled one in 2^k by the **per-word key**, the engine export `wordKey` (dev2, m152; xmesh-grid-web dev2/word-key fae4db6, in review with dev3 as the engine's owner):
        - `wordKey(w)` is FNV-1a 32 (offset 2166136261, prime 16777619) over the UTF-16 code units of `${from}|${to}|${sentTick}|${square}|${type}`, as an unsigned 32-bit number. It reads nothing else: not `seq`, `arriveTick` or `cmb`.
        - A word is drawn at shift k when k = 0, or when `Math.imul(wordKey(w), 0x9E3779B1) >>> (32 − k) === 0` for k ≥ 1. (In JavaScript a shift by 32 is a shift by 0, so k = 0 is stated on its own.) k comes from the view's own count of words to the viewer's mesh, with the renderer's hysteresis.
        - Test vectors: `{from: "sym-A1", to: "sym-A2", sentTick: 100, square: "x3y4", type: "ground"}` → 3287510634; `{from: "gw-A", to: "sym-B7", sentTick: 0, square: "x120y48", type: "station"}` → 3862597558. Both were checked against the definition in JavaScript and in Python.
        - Words whose five fields are all alike share a key, fly the same path, and draw the same picture either way (782 of 32,550 at 50 a side).
      - The key is stable over the word's flight, the same for every viewer, and blind to other meshes' traffic. The host and the console run the same export.
      - Never by `seq`. The engine numbers every mesh's words from one counter, so seven more hidden rival words renumber ours and change the sample (dev2's probe, m147: the renderer's gates held against moved, rewritten and removed rival syms, and failed only here).
  - `sim-second`:
    - events by kind per cell
    - `scope` (`viewport`, `mesh` or `all`), with each listed event carrying the engine's payload in `data`: verbatim on an x-ray view, and on a picture view without `seq` or any other number drawn from a world-wide counter (§7)
    - `counts` of every event this second, by mesh then kind
    - `dropped`: how many events in scope a cap left out, never a silent cut
    - `sides` (up, drained, joined, calls), `acts` (syms by mesh and activity), and the `front` per row band with every crossing, so a pocket shows
    - `diagnosticDigest`, on an x-ray view only (null on a picture view: a digest of the run's counters changes with traffic the viewer may not see)
  - The digest is **diagnostic only**: a cheap digest of counters, never a verified or final state.
  - `sim-checkpoint {basis: "truth", tick, fingerprint, final}` gives the full fingerprint at each run's end, and every 60 s only for runs small enough to save. It goes out on an x-ray stream only: a fingerprint digests the whole world (§7).
  - There is no per-second fingerprint at scale (dev3, m132): a fingerprint serializes the whole save, and the save fails at 1,000 spread syms.
  - A run whose save cannot complete reports `fingerprint {state: "unavailable", reason}`. No digest stands in for it and no receipt follows.
  - `part=square` returns the mesh's picture of a square, and a station's knowledge tallies with its dispute grouped by claim. `part=sym` returns the sym's card.
  - These are dev2's (m134), dev4's and dev5's stated needs.
- **A run's outcome** is the engine's judgement: `done` (won), `failed` (lost), or `void` (dev3, m149).
  - `void` means judged at the deadline with a goal fact that measured nothing, and `na` names those facts. It is neither won nor lost, and it is a result, not a failure of the host.
  - `byArm` counts void runs apart. Any comparison of arms (win counts, a sign test) leaves them out of both arms and reports how many.
  - Only Harbour Run's facts can measure nothing (engine dev3/harbour-orders ac16e55), so no shipped mission produces `void` yet. A consumer must still handle it.
  - `unjudged` means the run played to its end and the engine gave no judgement: `unjudged {reason, deadline}`, with `at` null.
    - `no-mission`: the world has no mission, so nothing is judged. It is a metrics-only run, and `deadline` is null.
    - `before-deadline`: the mission's deadline lies past the run's `secs`. More `secs` fix it. `deadline` names the deadline, and it is always later than `secs`: a deadline inside the run is one the engine judges.
    - It is not `void`. `void` is a judgement at the deadline, and `unjudged` is the absence of one. It is never a win or a loss, and `byArm` counts it apart. Its fingerprint and metrics are those of any run played to its end.
  - `queued` means it is waiting for a worker behind the host's `runsAtOnce` and has not started; it is never reported as `running`. `running` means it is still playing.
  - `stopping` means a `DELETE` or the host's ceiling ended it and the host has not yet reaped its worker. `stopped` means it has: `stopped {reason, by, tick}`, where `tick` is the last tick the worker acknowledged, never a guess (§6). A stop that cannot complete stays `stopping`, and the mission's `fault` says so.
  - `aborted` means the host ended it with no judgement: `aborted {reason: "worker" | "host-restart", tick}`, with the fingerprint `unavailable`. It is never counted as `failed`, which is the engine's verdict of a loss. After a host restart, `tick` is the last tick the host had recorded, which can trail the worker's last acknowledgement.
  - A run's first terminal state is kept: a completion that races a `DELETE` keeps whichever came first, and nothing later overwrites it.
  - A comparison of arms is computed on judged runs only, `done` against `failed`: that is its denominator. `void`, `unjudged`, `aborted`, `stopped` and `stopping` runs never count as wins or losses, and they stay in `byArm` under their own status, so every attempted run is reported and reliability stays visible. A paired comparison (a sign test across seeds) uses only the seeds judged in both arms, and reports the seeds it excluded and its ties. A win count on its own does not establish robustness.
- **Plots and receipts:**
  - Every plot carries the caller's `id`, unique within the mission. The engine keeps it on the plot's record as `ref`, so a saved run says which request made each plot.
  - Plots that apply at the same tick go in the order the request lists them. The engine numbers its records in that order, and a string `id` is never sorted.
  - Each run reports a `PlotReceipt {id, tick, outcome, reasons, events}`: the tick the host applied it at, `applied` or `refused` with the engine's reasons, and the seqs of the events it caused.
  - A replay receipt, the only thing that makes a simulation result *verified*, carries:
    - the requested inputs (scenario id and fingerprint, seed, secs, gear)
    - the arm
    - every plot as recorded
    - the session history
    - the exact engine build and commit (dev3 and codex-win's rules, m137)
- **Replaying a run elsewhere** (a console's replay link; founder, 29 September: "Replay now, live after"). The result names what a replay starts from:
  - `batch` (the host's `batch-<uuid>`) and each run's `run` (`batch-<uuid>.<n>`), beside its seed and arm.
  - `ceiling`, the batch's crew ceiling: a growth arm's help block is built with it. It is `null` when the host does not serve it.
  - Each `byArm` row's `played {fingerprint}`, the scenario as that arm played it, since an arm changes the scenario (`silent` turns peers off, `none` sets range 0, growth adds the help block). It is `null` while the host does not serve it. `null` means "not served", never "the same as `scenario`".
  - A console replays from the run's recorded inputs and history (its plots with their ids and ticks, and any session ends, setups or upgrades), as the host's own verification replays them. It never trusts a plot's recorded outcome: it works it out again.
  - It rebuilds the armed scenario and checks it against `played` before it plays. Until `played` is served, it can check only the unarmed `scenario` fingerprint, and it says so.
  - It replays on `executor {engine, commit}` or says it cannot. The run's final fingerprint then proves the match on that build.
  - This is audit data, like the fingerprints it rests on: it is inside `result`, so only the principals an x-ray could be granted to receive it. A replay shows the whole world.
  - Following a run live, while it plays, is not served yet. It needs the host's keyed route, relayed through the mission's stream.
- **Event names** come from the engine's event kinds. The battlefield spec's section 8 names (`station.taken`, `plot.*`, `help.*`, `base.*`) are used verbatim once they exist in the engine.

## 11. Errors

Every non-2xx body is `ApiError`: `{v: "xmesh.error/1", error: {code, message, details?, retryable, retryAfterS?, hint?}}`. That includes malformed JSON (`400`) and an oversized body (`413`); today both return Express's HTML pages.

| Code | HTTP | Example |
|---|---|---|
| `bad_request` | 400 | malformed JSON |
| `validation_failed` | 422 | schema refusal, with `details[{path, message}]`; the engine's own `validateScenario` reasons arrive the same way |
| `unauthorized` / `forbidden` | 401 / 403 | no session / role below the need |
| `not_found` | 404 | unknown id (a mission you cannot see is also 404) |
| `idempotency_conflict`, `invalid_transition`, `engine_mismatch` | 409 | — |
| `payload_too_large` | 413 | body over 256 KiB |
| `budget_exceeded` | 402 | a room over its daily budget, or today's spend cap reached (the hint names where a validator changes it) |
| `rate_limited` | 429 | with `retryAfterS` |
| `kind_unavailable` | 501 | e.g. a simulation on a deployment with no engine |
| `ownership_lost`, `unavailable` | 503 | this daemon cannot run work |
| `internal` | 500 | — |

## 12. Internal plumbing and security

These are not a user API: they are undocumented for users and absent from any public index. They still carry explicit rules:

| Route | Auth | Rules |
|---|---|---|
| `POST /api/v1/_/session` `{credential, seat?}` → `{token, user}`; `DELETE` revokes the caller's token → `{ok: true}` once the revocation is on disk, or 503 `{ok: false, error}` when the sessions file could not be written (the token is dead in this runtime, and a restart before the file's next successful write brings it back) | none to call (`DELETE`: the session it revokes); throttled per source (429 with Retry-After) | the credential is never logged; tokens are opaque and revocable |
| `GET /api/v1/_/health` | none | `{ok, ready, grounded}` only. An authenticated or loopback caller also gets `runtime {version, tree, restartOwed, startedAt}`. Never a path or a count. |
| `GET /api/v1/_/events` (SSE) | session | the cockpit's live mesh stream (today `/api/mesh-events`) |

Transport rules:
- TLS is required off loopback; Android refuses cleartext.
- No CORS by default.
- The JSON body limit is 256 KiB.
- Every response carries a request id.

## 13. What happened to the legacy routes (0.10.0)

Of the 152 `/api/…` registrations in 0.9.50's server, 57 are deleted and 95 move under `/api/v1/_/`; none is left at its old path.

**Folded into `/api/v1/mission`:**

| Today | Becomes |
|---|---|
| `/api/worlds*`, `/api/commissions*` (17 registrations), `/api/missions*` | POST, GET, DELETE and PATCH ops, as mapped in §4 |
| `GET /api/ask` | `kind: question` |
| `/api/recipes/review`, `/api/recipes/integration-assessment` (plus defaults) | kinds `review` and `assessment`; the defaults move into `capabilities` |
| `/api/approvals*` | op `decide`, and `evidence.requests` |
| `/api/backlog*` (11) | kind `task`, its ops, and `view=board` |
| `GET /api/query` | `view=recall` |
| `GET /api/canon` | `view=canon` |
| `GET /api/lanes` | `view=capabilities` |
| `POST /api/lifecycle/:key` | op `verdict` |
| `GET /api/artifact/:key` | `part=artifact` |

**Deleted, 57 registrations in all:** the legacy mission routes folded above, and
- the 10 routes marked **delete** in §13.1 (strict and loose search across five repos; station-artefact not searched)
- `POST /api/uitest/*`
- the `/api/_test/*` hooks (test builds only)
- the 410 retirement guard, the `/api/missions` rewrite and `LEGACY_PREFIXES`
- every comment that narrates the old spellings

Their history stays in git. The web cockpit that lived in this repository (`web/`) is deleted too: Station is the UI, served when a deployment names it with `XMESH_WEB_DIST`.

**Moved under `/api/v1/_/`, 95 registrations:** every admin and diagnostic route our own apps call (§13.1, founder's decision §14.2), unchanged except for the path, with their session and role checks. That includes the routes only another app calls (Station's automations, requests, judgement, conversations and spend; the iOS engine's overlap, reliability, verdicts, outcome and app events) and the plumbing (§12): session, health, readiness, version, and the live events, which moved from `/api/mesh-events` to `/api/v1/_/events`. `GET /api` says where the one public resource is.

**Kept for other runtimes:** `POST /api/v1/_/gateway/ingest`. §13.1's search covered client repositories, so it found no caller; the caller is another runtime's gateway projecting to this one (`server/gateway.ts`), and both ends now share one path constant.

**What a client does:** our own clients moved in this change (the CLI, the MCP bridge, the lifecycle commands, scripts, `hooks/cc-capture.mjs`, the Dockerfile and Kubernetes health probes). A runtime older than 0.10 answers only at `/api/version`, so the version probes used during an update ask both paths. Station migrates in its repository (branch `dev1/api-v1`); symday-webapp is retired (28 September) and is not migrated; the iOS apps (sym-mesh-engine, xmesh-ios) need their owners.

### 13.1 Admin and diagnostic routes, classified (codex-win's rule: callers, privilege and effect first)

Callers were found by matching each route's full path pattern, parameters included, against every literal `/api/…` string in xmesh (web cockpit, bin, mcp, scripts, hooks, worker, src, skills), xmesh-station, sym-mesh-engine, symday-webapp and xmesh-ios, on 27 September. Then every route with no match was searched again loosely.
- station-artefact is not on the checking host, so it was not searched.
- Where one path serves several methods, the call sites are shared, because a literal path does not say which method it is used with.
- A path built by concatenating strings could hide a caller from the strict match; the loose search is the backstop.
- Being undocumented is never access control: every internal route keeps its session and role checks.

Totals: 73 internal, 10 deleted, 4 fold or candidate fold. The list is the classification the change followed; §13 says what was built.

| Method and path | Needs | Effect | Callers (call sites) | In 0.10.0 |
|---|---|---|---|---|
| `GET /api/readiness` | session | read: why not ready | Station 2 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/me` | session | read: current principal | xmesh 1, Station 2, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/rooms` | session | read: rooms, residents, tallies, policy | xmesh 1, Station 3, sym-mesh-engine 5 | internal `/api/v1/_/…`, role check kept as today |
| `PUT /api/rooms/:name/policy` | validator | **write**: set room policy (minds, models, budget) | xmesh 1, Station 1 | internal `/api/v1/_/…`, role check kept as today |
| `DELETE /api/rooms/:name/policy` | validator | **write**: clear room policy | xmesh 1, Station 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/rooms/:name/preserve` | validator | **write**: preserve an x- room | Station 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/rooms/:name/retire` | validator | **write**: archive a room | Station 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/rooms/:name/budget` | session | read: room budget view | xmesh 1, Station 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/rooms/:name/unretire` | validator | **write**: restore a room | none found | **delete** (no caller: strict and loose search, 5 repos) |
| `GET /api/rooms/:name/members` | session | read: roster and gate facts | none found | **delete** (no caller: strict and loose search, 5 repos) |
| `GET /api/conversations` | session | read: conversation turns of a room | Station 2 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/bootstrap` | session | read: real-mesh state | xmesh 1, Station 2, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/cmb/:key` | session | read: one resolved CMB | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/attestations/:key` | session | read: gating attestations | xmesh 1, Station 2, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/lineage/:key` | session | read: lineage graph | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/decisions` | session | read: recent admission decisions | none found | **delete** (no caller: strict and loose search, 5 repos) |
| `GET /api/issues` | session | read: issue clusters | xmesh 1, Station 3, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/issues/fit` | session | read: which agents fit one issue (`q`) | missed by the first survey; served at `/api/v1/_/issues/fit` (dev2, m050) | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/cohering` | session | read: cohering trace | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/adaptation` | session | read: adaptation recommendations | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/adaptation/approve` | session | **write**: approve an adaptation | sym-mesh-engine 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/directive` | session | **write**: emit an operator CMB into a room | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/preflight` | session | read: dry-run readiness for a room | none found | **delete** (no caller: strict and loose search, 5 repos) |
| `POST /api/requests/parse` | session | read: CAT7 requirement capture | Station 2 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/edition` | session | read: sym.day edition | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/register` | session | read: blocker register | xmesh 1, Station 3 | fold: `view=register` (the blocked missions and their stated reasons) |
| `POST /api/spend/choice` | validator | **write**: choose spend protection | xmesh 1, Station 2 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/spend` | session | read: spend state | none found | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/attachments` | session | **write**: upload a file (<=15 MB) | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/app-events` | session | **write**: app telemetry events | sym-mesh-engine 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/authority/ledgers` | session | read: attestation ledgers | none found | **delete** (no caller: strict and loose search, 5 repos) |
| `POST /api/authority/import` | session | **write**: import attestation gossip | none found | **delete** (no caller: strict and loose search, 5 repos) |
| `GET /api/liveness` | session | read: liveness streak | xmesh 1, Station 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/calibration` | session | read: agent calibration | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/users` | validator | read: list users | xmesh 2, Station 2, symday 2 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/users` | anchor | **write**: create user | xmesh 2, Station 2, symday 2 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/users/:id/role` | anchor | **write**: change role | xmesh 1, Station 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/users/:id/deactivate` | anchor | **write**: deactivate user | xmesh 1, Station 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/learning` | session | read: learning tier | xmesh 1, Station 2, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/license` | session | read: licence | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/overlap` | session | read: divergence pairs | sym-mesh-engine 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/reliability` | session | read: worker reliability | sym-mesh-engine 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/teams` | session | **write**: staff planner+doers+critic | none found | **delete** (no caller: strict and loose search, 5 repos) |
| `POST /api/workers/:name/preserve` | validator | **write**: worker becomes a cognition node | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/workers` | session | read: hosted workers | xmesh 3, Station 3, sym-mesh-engine 2, symday 2 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/workers` | session | **write**: start a hosted worker | xmesh 3, Station 3, sym-mesh-engine 2, symday 2 | internal `/api/v1/_/…`, role check kept as today |
| `DELETE /api/workers/:name` | session | **write**: stop a hosted worker | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/first-mission/deploy` | session | **write**: deploy a node map | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/soak` | session | read: soak counters | xmesh 1, Station 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/proposals` | session | read: harness resource proposals | xmesh 1, Station 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/proposals` | session | **write**: create a proposal | xmesh 1, Station 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/proposals/:key/commit` | validator | **write**: commit a proposal | Station 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/proposals/:key/rollback` | validator | **write**: roll back a proposal | Station 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/eod` | session | read: end-of-day batch | xmesh 2 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/eod` | anchor | **write**: run end-of-day | xmesh 2 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/judgement` | session | read: judge records | Station 2 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/verdicts/:key` | session | read: per-node verdicts | sym-mesh-engine 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/audit` | session | read: admission audit | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/outcome/:key` | session | **write**: ground a CMB (verified/failed) | sym-mesh-engine 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/capture/start` | session | **write**: start session capture | xmesh 1, xmesh-ios 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/node/:name/cmbs` | session | read: a node's store | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/node-stats` | session | read: node tallies | Station 1, sym-mesh-engine 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/automations` | session | read: recurring missions | Station 2 | internal now; candidate to fold as a scheduled `work` mission |
| `POST /api/automations` | session | **write**: create recurring mission | Station 2 | internal now; candidate to fold as a scheduled `work` mission |
| `DELETE /api/automations/:id` | session | **write**: delete recurring mission | Station 2 | internal now; candidate to fold as a scheduled `work` mission |
| `GET /api/scheduler` | session | read: scheduler telemetry | Station 3 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/observed` | session | read: observed CMBs | xmesh 1, Station 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/observed/clear` | session | **write**: clear observed CMBs | xmesh 1, Station 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/conformance` | session | read: MMP conformance | xmesh 1, Station 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/mesh-rooms` | session | read: mesh rooms | xmesh 1, Station 2, sym-mesh-engine 2, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/join` | session | **write**: join a room by contribution | xmesh 1, Station 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/mesh-status` | session | read: bridge snapshot | xmesh 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/agents` | session | read: hosted agents | xmesh 4, Station 3, sym-mesh-engine 2, symday 2 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/agents` | session | **write**: start a hosted agent | xmesh 4, Station 3, sym-mesh-engine 2, symday 2 | internal `/api/v1/_/…`, role check kept as today |
| `DELETE /api/agents/:name` | session | **write**: stop a hosted agent | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `GET /api/gateway` | session | read: gateway state | xmesh 1, Station 1, sym-mesh-engine 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/gateway/peers` | licence (402 without) | **write**: add gateway peer | xmesh 1, Station 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/gateway/ingest` | licence (402 without) | **write**: ingest across a boundary | another runtime's gateway (`server/gateway.ts`) | internal `/api/v1/_/gateway/ingest`: the search covered client repos, and this caller is a runtime |
| `POST /api/gateway/project` | licence (402 without) | **write**: project across a boundary | sym-mesh-engine 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/connect-agent` | session | **write**: virtual sym node | xmesh 1, Station 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/canon/elevate/:key` | session | **write**: elevate canon row | xmesh 1, Station 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `PUT /api/lanes/:name` | validator | **write**: define a lane | Station 2 | internal `/api/v1/_/…`, role check kept as today |
| `DELETE /api/lanes/:name` | validator | **write**: remove a lane | Station 2 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/lanes/:name/probe` | validator | **write**: probe a lane | none found | **delete** (no caller: strict and loose search, 5 repos) |
| `GET /api/policy` | session | read: approval policy | xmesh 1, Station 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `PUT /api/policy` | anchor | **write**: set approval policy | xmesh 1, Station 1, symday 1 | internal `/api/v1/_/…`, role check kept as today |
| `POST /api/policy/grants` | anchor | **write**: add standing grant | none found | **delete** (no caller: strict and loose search, 5 repos) |
| `DELETE /api/policy/grants` | anchor | **write**: remove standing grant | none found | **delete** (no caller: strict and loose search, 5 repos) |
| `POST /api/v1/_/stop` | anchor, from loopback, with no forwarding header | **write**: drains in-flight missions, releases the lease, stops the runtime | xmesh 1 (`xmesh stop` on Windows) | new in 0.10.3, internal. Body `{expect: {pid}, reason?}`, the pid a precondition: 202 `{stopping, pid, inFlight, drainMs}`, a repeat 202 with `already: true`, 409 `{pid}` when the pid differs. Loopback is read from the socket, never from `req.ip`; a request carrying `Forwarded`, `X-Forwarded-For` or `X-Real-IP` is refused. The limit: a same-host proxy that adds no such header cannot be told apart, and then the anchor credential is what holds. |

**Clients migrated in the same change:** `bin/cli.mjs`, `bin/xmesh.js`, `bin/lifecycle.mjs`, the MCP bridge, the scripts (`verify-verdict-routing`, `reconcile-verdict-weights`, `deployed-version-guard`, `release-smoke`, `perf-gate`, `w4-bench`, `recall-eval`, `cohering-trace`, `adaptation`, `demo-cross-device-ask`), `hooks/cc-capture.mjs`, the Dockerfile and Kubernetes health probes, and the README. The web cockpit was deleted rather than migrated. Of the external apps counted above, Station migrates in its own repository, symday-webapp is retired and is not migrated (its counts record where each call was found on 27 September), and the iOS apps need their owners.

## 14. Open decisions

1. **"Only one mission allowed": resolved.** The founder: "One request one mission". It means one request makes one mission (§5); it is not a limit on how many missions run at once. The earlier placeholder, `capabilities.concurrency` with its `concurrency_limit` error, is removed. Waiting for capacity stays a per-mission state (`waiting.on: capacity`).
2. **The admin and diagnostic routes our apps call: decided** (founder, 27 September: "Ok follow your recommendations"). They are classified route by route in §13.1.
   - 73 move under `/api/v1/_/`, authenticated and role-checked exactly as today, undocumented, and not a user API.
   - 10 with no caller are deleted. The eleventh, gateway ingest, has a runtime as its caller and moved instead (§13).
   - `view=register` folds in now, and automations are a candidate to fold as scheduled work.
3. **Hosting simulations: decided, one engine host** (founder, 27 September: "Ok follow your recommendations").
   - The runtime hands `kind: simulation` to xmesh-grid-api as its simulation executor. The engine stays vendored once, there, and does not ship inside the npm package.
   - Users still see one xmesh: one API, one MCP tool, one CLI verb.
   - Merging the two servers can follow once the grid API's own scope (the game's shop and player services) is settled.
   - The run interface is agreed with the grid API's simulation owner, dev3 (m132), with seven amendments:
     1. Limits on seeds × secs × starting syms are refused with 422, never clamped. A pinned `engine` must equal the host's exactly, or 409.
     2. The arms served are those the engine supports. Growth needs the help cascade and the strict silent arm needs 0.9.17 on main; until both land, only comms mesh or none, with growth off.
     3. The only plot kind is `invaders`, not yet landed. A refusal at apply time is a recorded outcome in the run, not an HTTP error.
     4. There is no per-second fingerprint at scale: a fingerprint comes at each run's end, plus checkpoints every 60 s for small runs. `keep: "save"` is refused above what the host can save.
     5. The host computes frames and aggregates; the runtime only re-sequences them.
     6. Each simulation runs in its own worker process, with a heap ceiling and a CPU budget. A runaway is stopped and recorded as `stopped {reason, by: "host-ceiling", tick}`; a worker that crashes, or a host restart, ends a run as `aborted`.
     7. Replay receipts for plotted or multi-session runs need `/v1/runs/verify` to take the run's history (session ends, setups, upgrades, plots).
   - Ownership: dev3 owns the grid API's simulation side; its admin console and inbox are dev2's, pending dev2's confirmation.
   - Where `/v1/runs` sits in dev3's queue is the founder's call.
   - Until it lands, `kind: simulation` answers `501 kind_unavailable` and `capabilities` says why.
4. **Binding `spec` into the signature** waits for MMP v2.0 emission (§2).
5. **Timestamps** become ISO 8601 UTC strings in v1 (today they are millisecond numbers).
6. **One scenario definition, owned by the engine: done by dev3** (m135).
   - Where it lives: xmesh-grid-web `dev3/scenario-shape` 8ac7d24 (build 0.9.17+shape), `packages/engine/schema/scenario.schema.json`, `SCENARIO_SCHEMA_VERSION 1`, `$id https://xmesh.dev/schema/scenario/1`.
   - It covers structure only; the value rules stay in `validateScenario(sc, {closed: true})`, which never throws.
   - This API no longer copies either. The scenario and gear are objects the simulation host validates, and the host's reasons become `422` details.
   - It reaches main after the 0.9.17 landing and dev2's glimpse re-baseline, both waiting on the founder's push. Until then, pin 8ac7d24.
   - Plots are `{id, t, kind, params}`, with kinds from the engine's `PLOT_KINDS`. Arms are `{comms, growth}`.

## 15. The MCP server and the CLI: one way to do each thing

**MCP server.** The binary stays `xmesh-mcp`. `xmesh` wires it into Claude Code automatically, and `xmesh mcp install` does it by hand. Codex registers the same command in its MCP config.

The bridge has **one tool, `mesh_mission`** (founder ruling, 27 and 28 September 2026: one resource, one tool). Its `action` says what it does:

| Action | Does | Calls |
|---|---|---|
| `create` | hand the mesh a mission of any kind: `{kind, focus, criteria, spec}`, which the bridge sends as the `MissionRequest` of §2 (`focus` and `criteria` become CAT7 focus and commitment) | `POST /api/v1/mission` |
| `read` | one mission by id, or one part of it (`part`, `path`) | `GET /api/v1/mission/{id}[?part=…]` |
| `list` | your missions, filtered by `kind`, `status` and `query` (the list's `q`) | `GET /api/v1/mission` |
| `operate` | `{id, op, …}`, a `MissionOperation` (§4) | `PATCH /api/v1/mission/{id}` |
| `abandon` | `{id}` | `DELETE /api/v1/mission/{id}` |
| `view` | `view: "board"` (the task board), `"recall"` with `query` (search what the mesh knows, along one CAT7 field), `"canon"` (the validated knowledge that leads recall) or `"capabilities"` (kinds, engine and lanes) | `GET ?view=…` |

- `mesh_ask`, `mesh_review`, `mesh_missions`, `mesh_board`, `mesh_recall`, `mesh_canon` and `mesh_lanes` are removed: each is an action of `mesh_mission`.
- A limit past what an action allows is refused, never clamped: `list` at most 200, `view recall` 60, `view canon` 500.
- One tool means one permission: a client that allows `mesh_mission` without asking also lets it start missions.

It returns the mission. For `question`, it waits up to `spec.wait` seconds and returns the answer if it is ready.

**CLI.**
- `xmesh mission "<focus>" [--kind work|question|review|research|assessment|task|simulation] [--criteria "<CHECK: …>"] [--room R] [--origin-room R] [--project DIR] [--target FILE] [--wait S] [--scenario LIB|FILE.json] [--seeds 1,2,3 | --count N] [--secs S] [--arm mesh|silent|none[+growth]] [--lint] [--json]` hands the mesh a mission and prints its id and status.
- `xmesh mission <id> [--part outputs|diff|log|artifact|root-cause] [--path P]` shows a mission's status and result.
- `xmesh mission <id> --abandon | --verdict validated|dismissed | --decide <request> approve|deny | --resume | --replay | --apply | --reskill <skill> | --next | --take <n> | --root-cause` operates on it.
- `xmesh missions [--status S] [--kind K] [--room R] [--view board|register] [--json]` lists missions.
- Kept for operators: `status`, `soak`, `eod`, `breaks`, `proposals`, `workers`, `budget` and `deploy-check`, plus the server commands: `xmesh`, `update`, `restart`, `stop`, `mcp install`, `--version` and `help`.
- Removed, each folded into `xmesh mission` or `xmesh missions`: `ask`, `offer`, `lint`, `detail`, `rule`, `replay`, `abandon`, `register`, `review`, `assess` and `research`.

**Ships in** xmesh **0.10.0**. It breaks the API, so the minor version moves, and it stays 0.x. The release follows codex-win's verdict, the implementation, the tests, and the migration of every client (founder: "release once all ready").
