@cotal-ai/pi 0.97.1 → 0.98.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +5 -5
- package/dist/standalone.js +5 -5
- package/package.json +3 -3
package/dist/standalone.js
CHANGED
|
@@ -65199,10 +65199,10 @@ function date4(params) {
|
|
|
65199
65199
|
config(en_default());
|
|
65200
65200
|
|
|
65201
65201
|
// ../connector-core/dist/docs-bundle.generated.js
|
|
65202
|
-
var DOCS_VERSION = "0.
|
|
65202
|
+
var DOCS_VERSION = "0.98.0";
|
|
65203
65203
|
function loadDocsBundle() {
|
|
65204
65204
|
return {
|
|
65205
|
-
"version": "0.
|
|
65205
|
+
"version": "0.98.0",
|
|
65206
65206
|
"generatedFrom": "docs/*.md + SPEC.md + spec/cotal-lang.md + spec/cotal.schema.json",
|
|
65207
65207
|
"pages": [
|
|
65208
65208
|
{
|
|
@@ -65273,7 +65273,7 @@ function loadDocsBundle() {
|
|
|
65273
65273
|
"title": "`cotal` CLI reference",
|
|
65274
65274
|
"kind": "Reference: describes the TypeScript reference implementation (the `cotal` CLI), not the wire contract.",
|
|
65275
65275
|
"summary": "cotal is the operator command line for the reference implementation: bring a mesh up, mint identities, launch agents, watch what they do, and tear it all down.",
|
|
65276
|
-
"body": "# `cotal` CLI reference\n\n> **Reference**: describes the TypeScript reference implementation (the `cotal` CLI), not the wire contract. \xB7 **For:** operators \xB7 **Wire contract:** [SPEC](../SPEC.md)\n\n`cotal` is the operator command line for the reference implementation: bring a mesh up, mint\nidentities, launch agents, watch what they do, and tear it all down. It is a thin client over the\nwire contract: the normative subjects and schemas live in the [SPEC](../SPEC.md); this page is\nlookup material for the commands, not a walkthrough; if you are new, start with\n[Getting started](getting-started.md).\n\n## Running it\n\n```bash\nnpm install -g cotal-ai # puts `cotal` on your PATH (needs Node 22+)\ncotal # the selected space, common commands, and where to start\ncotal help # compact actions and selected context (also `cotal --help`)\ncotal help --all # every command group, including hidden commands\ncotal help <command> [<subcommand>] # one command's or subcommand's usage and flags\ncotal version # cotal-ai version + each installed extension's (also `cotal --version`, `cotal -V`)\ncotal version --json # the same as one JSON document (also `-j`)\n```\n\nThe shell installer prints its absolute launcher path. Use it when an older installation still\nwins plain `cotal`. Having `~/.local/bin` somewhere on PATH does not establish precedence, and a\nchild installer cannot change the parent shell's command cache, aliases or functions. Inspect\n`type -a cotal` in that shell. After putting the new directory first, refresh its command cache\nwith `hash -r` (or `rehash` in zsh). An alias or function must be corrected separately. To restore\nconnectors after an old executable rebuilt them, run the verified newer absolute executable with\n`ext seed`, without `--force`.\n\n`npx cotal-ai <command>` runs it without a global install. Bare `cotal` starts nothing: it names\nthe selected space, lists common commands, and points a machine with no selected space to\n`cotal setup`. Every command generates its own help, usage, and shell completion from its declared\nflags, and `cotal <command> --help` prints the same help as `cotal help <command>`. `-v` is not a\nversion flag.\n\nSome commands have short aliases, listed in `cotal help --all`: `sp` (spawn), `a` (attach),\n`s` (status), `r` (run), `x` (ext), `h` (history), `cfg` (config) and `p` (persona). Subcommands can have their own, such as\n`cotal ext rm` for `cotal ext remove`. An alias behaves the same as its command. A command or\nsubcommand is never selected by a prefix, so `cotal spaw` is an unknown command. A command, alias,\nor extension command that would claim a word another one already claims is refused when it\nregisters or installs.\n\nIn a dev clone, `pnpm cotal <command>` runs `bin/cotal.ts` through `tsx`, but `bin/` loads the other\npackages from their built `dist/`. Run `pnpm build` after `pnpm install`, and again after you edit a\npackage, or the command fails with `ERR_MODULE_NOT_FOUND` for `@cotal-ai/cli/dist/index.js` or runs\nthe old code. A Windows source build needs the matching MSVC Developer Command Prompt to compile\nits first-party process identity binding. Published packages already contain the x64 and arm64\nbinaries and need no compiler or native download.\n\nCommands that change the installation run the automatic connector seed and stop when the\noperator-global seed store is not current for the checkout's version, which includes a fresh config\nand a store another version wrote. The error includes\n`this would migrate the machine-wide store and can take an installed cotal down`. A\ncheckout never writes the connector store an installed `cotal` uses. For day-to-day\nwork, set `COTAL_SKIP_CONNECTOR_SEED=1` to skip that seed step:\n\n```bash\npnpm install\npnpm build\nCOTAL_SKIP_CONNECTOR_SEED=1 pnpm cotal status\n```\n\nPointing `XDG_CONFIG_HOME` at a scratch dir is not enough by itself. Isolated release tests\nthat must seed from a checkout also set `COTAL_ALLOW_CHECKOUT_SEED=1`, as described under\n[Built-in connectors are seeded extensions](#built-in-connectors-are-seeded-extensions).\n\nBefore publication, the release workflow installs the packaged candidate into fresh homes and\ncaches on Linux, macOS and Windows, on x64 and arm64 with Node 22, 24 and 26. Each setup seeds\nconnectors, starts the broker, delivery daemon and Manager, checks native process identity and\nManager control, then stops and restarts them. Every platform must pass before normal or snapshot\npublication can proceed.\n\nAn undeclared flag is a usage error, and so is a flag given more than once unless it is\nrepeatable, as `--opt` and `down --session-store` are. So is an unknown command, and a first word\nthat is not one of the command's subcommands when it declares them. So is input a command checks\nbefore it acts, such as a missing argument, an invalid flag value or two flags that conflict, and\n`cotal help` with a subcommand that does not exist. The command prints the error, then any remedy,\nthen its help on stderr, writes nothing on stdout, exits 1, and does not run. A usage error is found before any effect, so it never seeds\nconnectors or writes configuration. Help, version and completion never do either.\n\nCommand output, including error lines on stderr, the guided `setup` and `meshes add` prompts and\nthe `console` screen, is colored only when stdout is a terminal, so piped or redirected output is\nplain text. A non-empty `NO_COLOR` turns color off on a terminal too. `FORCE_COLOR` turns color on even when output is\npiped, unless it is `0` or `false`, and it takes precedence over `NO_COLOR`. `--no-color` turns\ncolor off for that run, whatever the environment says. It sets no `NO_COLOR` for the programs the\nrun starts, but they are no longer forced into color by `FORCE_COLOR`.\n\nAt any question the CLI asks, Ctrl-D on an empty line or an input that closes cancels the question\nas Ctrl-C does, so the command reports its own cancelled outcome.\n`--no-input` means the run never prompts, opens an editor or opens a browser. A command that would\nprompt behaves as it does without a terminal, `web` prints its link without opening it, and\n`personas edit` is refused. `--json` implies `--no-input`. `--no-color` and `--no-input` work before\nthe command word on every command, and after it on every command except one that takes its words\nraw. After `--` they are ordinary words. No command may declare a flag with either name. The output\nflags `--json` (`-j`) and `--plain` may also come before the command word, as in\n`cotal --json status`; they are read as the command's own flags, so a command without that output\nrefuses them as it does after the command word. A command that takes its words raw, such as the\nhidden completion entry, has no flags to read them as and refuses them before its word with a usage\nerror. A `--` before the command word ends these leading flags. An installed extension\nwhose command declares one is an internal error, and `cotal` exits 1 before running any command.\nShell completion treats the current `--` word as a flag prefix until it is committed. After `-- `,\nit completes positional values without offering options. A string option's value is completed\nfrom its own candidates or file hint, without inserting other option names.\n\nCommands come from the surfaces the binary composes: the base mesh CLI, the manager and the\ndelivery daemon, plus any operator-installed extensions. Hidden entries such as the deprecated\n`supervise` and `deliver` are not completed.\n`cotal ext add <npm-package>` installs any registry providers a package contributes: commands,\nruntimes, and local process lifecycle descriptors. The `web` dashboard and optional manager\nruntimes ship this way. A `@cotal-ai/*` package named without a version installs this cotal's\nversion, as `setup` and `update` do; a version or tag you name is installed as given.\n\n## Structured output\n\nA command whose help has a `structured output` line naming `json` takes `--json` (`-j`) and writes\none JSON document on stdout. Most commands have one, among them `cotal status`, `cotal space`,\n`cotal service`, `cotal up`, `cotal down`, `cotal ps`, `cotal history` and `cotal config`.\n`cotal help <command>` shows whether a command and each of its subcommands has one.\n`cotal version --json` writes:\n\n```json\n{\"schemaVersion\":1,\"command\":\"version\",\"target\":null,\"outcome\":\"ok\",\"completeness\":\"complete\",\"result\":{\"version\":\"<version>\",\"extensions\":[{\"package\":\"@cotal-ai/web\",\"version\":\"<version>\"}]},\"warnings\":[]}\n```\n\n| Field | Meaning |\n|---|---|\n| `schemaVersion` | `1` |\n| `command` | The command that ran |\n| `target` | What it acted on, or `null` for a command that reads only local metadata |\n| `outcome` | `ok`, `no-op`, `refused`, `failed`, `partial` or `unknown` |\n| `completeness` | How much of the read scope the result covers: `complete`, `partial` or `not-checked`. It says nothing about whether the work succeeded |\n| `result` | The command's result. A `partial` read keeps what it read; a refused, failed or unknown run has `null` |\n| `warnings` | One `{code, target, message}` for each thing it could not read or check |\n| `error` | Present when the outcome is not `ok` or `no-op`: `{code, message, remedy, cause}`, where `cause` keeps an endpoint's own code, outcome and safe detail |\n| `changed` | What a command that did not succeed changed anyway |\n\nDiagnostics stay on stderr. A usage error under `--json` is a `refused` envelope with code\n`USAGE_INVALID`, and the help it names stays off stdout. A command without a JSON output refuses\n`--json` as an unknown option.\n\nA subcommand's help has its own `structured output` line, and only the forms that show one write a\ndocument. Bare `cotal ext` and `cotal ext list` do. `ext add`, `ext remove`, `ext root` and\n`ext seed` report as text, refuse `--json` and `--plain` with a usage error, and exit 1 on any\nfailure.\n\nA command whose `structured output` line also names `plain` takes `--plain`, which writes the same\nreport as plain lines: no color, no marks, ASCII labels. Plain writes these punctuation marks in\nASCII wherever they appear: `\xB7` and long dashes as `-`, `\u2026` as `...`, `\u2192` and `\u21B3` as `->`, and `\xA7`\nas `section`. Other characters, such as the letters of a name or a message, are written as they are.\nPass one of `--json` and `--plain`; both is a `USAGE_INVALID` refusal. A table that does not fit the\nterminal is written as one labeled record per row, so no value is cut off.\n\nShared human reports for a resolved space begin with `Mesh: <space>`, including\nempty `ps` results, manager inventories, the impact a command shows before it\nstops something, and classified failures. A failure raised after the command\nresolved its space carries that space in `target.space`, even when the failing\nstep named no space. The label comes from the operation's target, not a new read\nof the current selection. Global reports do not invent a selected mesh. JSON\nretains `target.space` without a text header. Plain watch feeds and followed\nhistory prefix each new row with the mesh name so the context remains visible as\noutput scrolls.\n\nOn a terminal, a long value, line, warning or failure wraps at spaces, and each continuation line\nstarts under the text it continues. A word wider than the line, such as a path, keeps a line of its\nown and is never split. Help wraps the same way, including the title of `cotal help --all`.\n\nA usage line breaks only between options, never between an option and its argument. A ` | ` stays\non the line before it whenever it fits there. A compact `|`, such as the one in\n`<persona.md>|--prompt`, is a break point too: the line ends right after it, the next line starts\nwith the following option, and no space is added or lost. A usage too wide for the line breaks after\nits own ` | ` or compact `|` alternatives first. A group too wide for the line, such as\n`(--to/-t <agent> | --channel/-c <channel> | --role/-r <role>)`, takes lines of its own and breaks\nthe same way inside, and so does a group nested in it. A group that fits, such as\n`[--json|--plain]`, stays whole. Only a single option with its argument that is wider than the line\nbreaks at its spaces. Examples are not wrapped, so each stays one line to copy.\n\nWhen the flag names leave their descriptions fewer than 30 columns, each description goes on the\nlines under its flag. A report's list of commands follows the same 30-column rule. Below 48 columns,\na record puts each label on a line of its own with its value under it. Output to a pipe or a file is\nnever wrapped.\n\nA read that could not read part of what it was asked for is `partial`: `result` holds what it read,\n`completeness` is `partial`, each part it could not read is a warning, and it exits 4. A list is\nnever presented as complete when part of it is missing.\n\nA target the command cannot resolve or reach is refused before any request, with the code for why:\n`TARGET_NOT_FOUND` (no such mesh), `TARGET_AMBIGUOUS` (several match), `TARGET_UNAVAILABLE` (its\nbroker or manager did not answer), `AUTH_REQUIRED` (it needs a sign-in or a fresh credential),\n`PERMISSION_DENIED` (it refused the credential), `DEPENDENCY_UNAVAILABLE` (trust material, the\nuser-auth service or a space catalog it needs could not be used), or `UNSUPPORTED`.\n\nA command with a JSON output exits by its outcome, with or without `--json`:\n\n| Outcome | Exit | Codes |\n|---|---|---|\n| `ok`, `no-op` | 0 | none |\n| `refused` before any effect, for invalid input | 1 | `USAGE_INVALID`, `SCHEMA_INVALID` |\n| `refused` at a known boundary before any effect | 2 | `PERMISSION_DENIED`, `AUTH_REQUIRED`, `TARGET_UNAVAILABLE`, `TARGET_NOT_FOUND`, `TARGET_AMBIGUOUS`, `DEPENDENCY_UNAVAILABLE`, `UNSUPPORTED`, `CONFLICT`, `LIFECYCLE_BLOCKED` |\n| `unknown`: it may have had an effect that cannot be attributed. Do not retry blindly | 3 | `OPERATION_UNKNOWN` |\n| `partial`: some of the work completed, or cleanup remains | 4 | `OPERATION_PARTIAL` |\n| `failed`: a confirmed failure with no cleanup left | 5 | `OPERATION_FAILED` |\n\nAn error such a command does not classify is `unknown`, exit 3. Other commands still exit 1 on any\nfailure.\n\n## Commands\n\n| Area | Command | Purpose |\n|---|---|---|\n| Set up & lifecycle | [`setup`](#setup) | Prepare a space, application and runtimes; opt into startup separately |\n| Set up & lifecycle | [`update`](#update) | Reconcile first-party extensions and check or opt into a coherent CLI upgrade |\n| Set up & lifecycle | [`up`](#up) | Start a local mesh (nats-server + JetStream), or boot a whole manifest with `-f` |\n| Set up & lifecycle | [`down`](#down) | Stop the whole stack, selected registered components, or a manifest deploy |\n| Set up & lifecycle | [`backup`](#backups) | Create an offline full-space or registry-only artifact from a preserved cut |\n| Set up & lifecycle | [`clean`](#clean) | Configurable cleanup: purge history (live), or wipe the local store / identity (stopped) |\n| Set up & lifecycle | [`reset`](#reset) | Preview or reset one stopped local store or space identity |\n| Set up & lifecycle | [`space`](#selecting-a-space) | List the spaces this machine can reach, or show one in full |\n| Set up & lifecycle | [`use`](#selecting-a-space) | Select the space commands target when none is named, or show the selection |\n| Set up & lifecycle | [`meshes`](#mesh-registry) | List, register and drop meshes; `space` replaces its listing |\n| Set up & lifecycle | [`sync`](#mesh-registry) | Refresh the signed-in account's advertised spaces |\n| Set up & lifecycle | [`status`](#mesh-registry) | Read-only diagnostics for setup, processes, and the selected mesh |\n| Agents & personas | [`spawn`](#spawn) | Launch an agent from a persona (foreground, or `--detach` via the manager) |\n| Agents & personas | [`models`](#models) | List connector model catalogs and variants from the manager |\n| Agents & personas | [`ps`](#managed-seats) | List managed agents and their mesh status |\n| Agents & personas | [`stop`](#managed-seats) | Ask the manager to stop a managed agent |\n| Agents & personas | [`attach`](#managed-seats) | Stream and drive a managed agent's terminal (pty runtime) |\n| Agents & personas | [`input`](#input) | Type one line into a managed agent's terminal without attaching |\n| Agents & personas | [`persona`](#persona) | List, show, edit, create, or remove local personas |\n| Agents & personas | [`supervise`](#supervise) | Deprecated and hidden from help: run a manager daemon; use the `manager` service preset |\n| Agents & personas | [`service`](#service) | Save, start, stop and inspect owned local services, from a preset or a definition file |\n| Agents & personas | [`daemon`](#daemon) | Linux: serve the local machine daemon's control endpoint, read its status, sign it in, check and request its own registration with a space |\n| Agents & personas | [`runtimes`](#runtimes) | List the agent runtimes the manager can spawn through and whether each is reachable |\n| Agents & personas | [`seats`](#seats) | List the pty seat custodians an earlier Linux manager left, and drain the ones whose agent has exited |\n| Agents & personas | [`endpoint deregister`](#endpoint-deregister) | Remove the service registration of an instance whose host is gone, when the broker affirms nothing is subscribed on its rail |\n| Agents & personas | [`reconcile-gate`](#reconcile-gate) | Unfreeze an issuance gate left frozen by a crashed restart when the successor cannot boot-heal it (holder gone, complete CONNZ sweep) |\n| Messaging & watching | [`who`](#who) | The live roster of the selected space, and whether a manager runs each seat |\n| Messaging & watching | [`whoami`](#whoami) | The identity this CLI acts under, and what it may read, post and do |\n| Messaging & watching | [`endpoints`](#endpoints) | List every endpoint in the live presence roster, including infrastructure |\n| Messaging & watching | [`describe` / `invoke`](#endpoint-control) | Resolve a v0.4 service's command surface off the wire; invoke one command by name |\n| Messaging & watching | [`send`](#send) | Send one message, then exit: DM a peer, post a channel, or ask a role |\n| Messaging & watching | [`chat`](#chat) | Open a live session under its own identity: terminal view, `--plain` lines, or `--stdio` for programs |\n| Messaging & watching | [`channels`](#channels) | Inspect or set the channel registry |\n| Messaging & watching | [`history`](#history) | Read retained messages, or clear them |\n| Messaging & watching | [`pending`](#pending) | Inspect one DM durable's native counts and bounded candidate IDs |\n| Messaging & watching | [`console`](#console) | Live protocol view for a space (TUI, or `--plain` line stream) |\n| Messaging & watching | [Web dashboard](#web) | Browser dashboard, run as the `web` service preset (installed as the `@cotal-ai/web` extension) |\n| Extensions & misc | [`linear`](#linear) | Serve the official Linear MCP server as an endpoint agents can call (installed as the `@cotal-ai/linear` extension) |\n| Auth & meshes | [`mint`](#mint) | Mint a creds file for a space (static auth mode) |\n| Auth & meshes | [`login`](#login) | Sign in to a per-user-auth mesh's IdP (once per machine) |\n| Auth & meshes | [`logout`](#login) | Revoke the IdP session and clear the cached login |\n| Auth & meshes | [`actor`](#actor) | Manage a user-auth space's actor ledger (grant / revoke / list) |\n| Auth & meshes | [`doctor`](#doctor) | Credential-health diagnosis and repair (`doctor auth`) |\n| Auth & meshes | [`join`](#join) | Join a space as your own presence (interactive) |\n| Manifest | [`topology`](#manifest-deploys) | Validate and view a mesh manifest's access graph (read-only) |\n| Extensions & misc | [`ext`](#ext) | List, install and remove operator CLI extensions |\n| Extensions & misc | [`docs`](#docs) | Read this version's protocol and CLI docs, offline |\n| Extensions & misc | [`config`](#config) | Show and set your preferences: the defaults your commands use |\n| Extensions & misc | [`completion`](#completion) | Report, print or install shell completion |\n| Extensions & misc | [`feedback`](#feedback) | Send feedback to the Cotal developers |\n| Extensions & misc | [`telemetry`](#telemetry) | Show or set the anonymous usage telemetry level (`off`, `minimal`, `regular`) |\n| Extensions & misc | [`deliver`](#server-daemons) | Deprecated and hidden from help: run the delivery daemon; use the `delivery` service preset |\n| Workflow runs | [`run`](#run) | Operate durable workflow runs: start, resume, list, inspect, answer a checkpoint, check an edited program with migrate |\n| Extensions & misc | [`feedback-intake`](#server-daemons) | Deprecated and hidden from help: run a feedback intake server; use the `feedback-intake` service preset |\n| Extensions & misc | [`auth-service`](#server-daemons) | Deprecated and hidden from help: run a user-auth space's auth service by hand; use the `auth-service` service preset |\n\nThe manifest modes of `up`, `spawn`, and `down` (`-f <cotal.yaml>`) plus `topology` are covered\ntogether under [Manifest deploys](#manifest-deploys).\n\n## setup\n\n```bash\ncotal setup [--destination <local|self-hosted>] [-H|--harness <connector>] [--runtime <name>]... [--project <dir>]\n [--space <name>] [--connection-file <bundle.json>] [--configure-only | --start [-d|--detach]]\n [--on <manager-instance>]\n [--demo|--full] [-y|--yes] [--json|--plain]\ncotal setup --skills\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--destination` | asked | `local`: a space this machine starts. `self-hosted`: a space registered here that runs on a server. `cloud` is refused as in development, before anything is written or contacted. |\n| `--harness`, `-H` | asked | The connector your agents run in. Setup runs its own setup actions and records it as the space's `harness` preference, so `cotal spawn` in the space launches it. When you have no `harness` preference of your own, it becomes yours too. |\n| `--runtime` | asked; none in a script | A runtime to set up besides the built-in `pty`: `tmux`, `cmux`, `orca`, `herdr` or an installed third-party one. Repeat it for each. |\n| `--project` | asked | The project folder: the space's root, whose `.cotal/agents` holds its personas. A script's folder must exist; a terminal offers to create it. |\n| `--space` | asked | The space to prepare (local) or the registered space to use (self-hosted). |\n| `--connection-file` | none | Self-hosted: register the space from a connection bundle exported where it runs, with the checks `space add --file` makes. |\n| `--configure-only` | on | Save preparation and start nothing. Refused with `--start`. |\n| `--start` | off | After successful preparation, start the local stack when needed and launch the default persona. Self-hosted setup uses its registered manager and starts no replacement local stack. |\n| `--on` | none | Select one self-hosted manager instance for startup. Local setup pins the manager it starts and refuses this flag. In a script, use it with `--start`. |\n| `--detach`, `-d` | off | With `--start`, leave the managed agent running without opening a terminal view. Required in scripts, JSON and plain mode. |\n| `--demo` | off | Also seed the guided expert team (`david`, `sven`, `me`) |\n| `--full` | off | The guided flow with the guided team (implies `--demo`) |\n| `--yes`, `-y` | off | Ask nothing: every input comes from the flags |\n| `--skills` | off | Reconcile Cotal skills only through installed connector providers, plus `~/.agents/skills`. Refused with the four inputs, `--runtime`, `--connection-file`, `--configure-only`, `--start`, `--on`, `--detach`, `--full`, `--demo`, `--json` or `--plain`. |\n\nSetup saves preparation before a separate startup decision. Configure-only is the default;\n`--yes` never turns it into startup. On a terminal it asks for each input no flag gave: destination,\napplication, runtimes, project folder and space.\nIt shows the answers before it writes: Save writes them, Change asks every question again with the\nanswers kept, and Cancel saves nothing. Cloud is listed as in development and cannot be chosen.\n\nWithout a terminal, or with `--yes`, `--no-input` or `--json`, every input is a flag. A missing one\nis `USAGE_INVALID` naming each missing flag, before anything is written:\n\n```bash\ncotal setup --destination local --harness claude --project ~/research --space research --no-input\n```\n\nA local space is prepared as `cotal space create` prepares it. A self-hosted space is one already\nregistered with `cotal space add` or a signed-in catalog, whose root must be `--project`, or one\n`--connection-file` registers. Setup then seeds the `default` persona into\n`<project>/.cotal/agents`, the catalog `cotal spawn` reads for that space, records the application as\nthe space's `harness` preference, runs the application's setup actions, and reconciles Cotal's\nskills. When you have no `harness` preference of your own, setup also records the application as\nyours, so a space you prepare later with `space create` uses it. A value you already set stays.\nThe result names the next command: `cotal up` in the project for a local space, or\n`cotal spawn --space <name>`. `--json` writes one envelope whose `result.written` lists every file\nsetup wrote.\n\nSetup needs an installed connector. With none, a `--harness` that names no official connector is\nrefused with `USAGE_INVALID`, exit 1, and otherwise setup refuses with `DEPENDENCY_UNAVAILABLE`,\nexit 2. Its remedy names `cotal ext seed` on a released install. A source checkout does not seed\nconnectors unless `COTAL_ALLOW_CHECKOUT_SEED=1` marks a sandboxed release test, and the remedy says\nso.\n\nAfter successful preparation, the terminal offers Start separately with No selected. An explicit\n`--configure-only` suppresses that question. `--start` skips it; scripts must also pass `--detach`.\nMissing application/runtime prerequisites keep preparation saved and do not launch anything.\n\n```bash\ncotal setup --destination local --harness claude --project <dir> --space <name> --start --detach --json\n```\n\nFor a local space, setup invokes the current native `up` in the prepared project, then pins the\nagent request to that root's recorded manager. Native up returning successfully is not reported as\nwhole-stack health. The managed launch must return its actual joined identity. Self-hosted startup\nuses the registered manager's persona catalog; `--on` pins its manager instance\nthrough credential issuance and launch. It uploads no local persona file. Without detach,\nthe terminal opens that allocated incarnation's native view. If it disappears before attachment,\nsetup reports the confirmed join and an incomplete view, not a healthy attached terminal. Leaving\nthe view sends no stop request.\n\n`result.startup` separates the requested self-hosted manager pin, native stack-command\nreturn, joined-agent receipt and any classified failure. A declined Start keeps the\npin in the suggested spawn command rather than silently selecting another manager. A joined result includes its full manager identity and a usable exact `attach`\ncommand when its returned tokens are safe to put in a shell command. Preparation remains saved\nafter a denied or uncertain start. Unknown outcomes are not retried; the setup heading is not\nshown as healthy and the next command inspects the original startup before attachment. JSON stdout has\none envelope; native startup output is not parsed or echoed as readiness evidence. Fresh prepared\nstatic-auth, open and plain-loopback user-auth roots use the controller stack described under `up`.\nManifest startup retains its native launch path.\n\nRuntimes are where agents run. `pty` is built in and always set up. The terminal lists the official\nruntimes and every installed third-party one, each with whether it is set up and whether the host\ntool it drives is on PATH, or named by the driver's own variable (`COTAL_ORCA_BIN`,\n`CMUX_BUNDLED_CLI_PATH`); installed ones and ones whose tool was found start selected. Setup\ninstalls each selected runtime as `cotal ext add` does, at this cotal's version, then asks its\nprovider whether it answers, as `cotal runtimes` reports. A selected runtime whose host tool is\nmissing is not installed, and one that does not install or answer leaves the rest saved and the\nresult `partial`. A script sets up only the runtimes `--runtime` names; an unknown name is\n`USAGE_INVALID` and a missing host tool is `DEPENDENCY_UNAVAILABLE`, both before anything is\nwritten. Setup never removes a runtime you leave out; `cotal ext remove` does. `result.runtimes`\nlists each one with its package, its host tool and whether setup found it set up, added it or left it\nout.\n\nA rerun with the same answers changes nothing and is a `no-op`; a space already prepared, started or\nregistered at that root is reported, not refused. A space name or root another space holds is\n`CONFLICT`. An application whose host tool is not on PATH leaves the rest saved and the result\n`partial` (exit 4), naming the tool; an application action that does not finish does the same.\n`cotal status` reports the machine's state, and it points\nstale Claude skills and out-of-date `.agents` skills at `cotal setup --skills`. See\n[Getting started](getting-started.md) and, for maintainers, [setup internals](setup-internals.md).\n\n## update\n\n```bash\ncotal update [--self] [--space <s>] [--server <url>] [--creds <path>]\ncotal update --check [-j|--json|--plain]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--self` | off | If a newer release exists, install that exact validated `cotal-ai` version globally and reconcile through the newly installed binary |\n| `--space`, `--server`, `--creds` | resolved mesh | Select the running manager whose continuity state is reported |\n| `--check` | off | Read version availability and recorded extensions without installation or restart; takes no mesh selector or `--self` |\n| `--json`, `-j`, `--plain` | off | Choose the finite check report; requires `--check` |\n\nUpdate reads the recorded mesh inventory without refreshing account space catalogues. An expired\nlogin to a remote mesh does not block the software update. Local manager continuity checks still\napply before changing the installation.\n\nWithout `--self`, `update` keeps the installed first-party surfaces coherent with the running\nbinary: it force-reconciles the four built-in connectors, brings what a connector installed in its\nharness to this release (the `cotal` plugin copy Claude Code sessions load, and its Claude Desktop entry), then reinstalls other `@cotal-ai/*`\noperator extensions at the binary's exact version. Each extension runs in an isolated child, so one\nfailure cannot poison later replays. It then checks npm; a newer binary is an informational notice\nwith `cotal update --self` as the next command, not an automatic install.\n\nBefore disk reconciliation, `update` reads the selected running manager. A machine with no recorded\nmesh has no running manager to observe, so that read is skipped and the command completes. The same\nholds when every recorded mesh is down and none is selected. A remote user mesh, registered with\n`cotal meshes add --mode user`, is named and skipped: its manager runs under another install, so\nthere is no custody on this machine to preserve, and a `legacy` verdict still comes only from a\nmanager this machine read. A stopped mesh that this machine runs, chosen with `--space` or by running\ninside its project, has no running manager either when no `cotal supervise` process on this machine\nmay be serving its space and nothing listens at its recorded broker address, so the read is skipped\nthere too. That is the cold path past a `legacy` verdict: `cotal down`, then `cotal update`. Any\nother selected mesh whose manager cannot be read is still a refusal: a named space that is not\nrecorded, a mesh registered with `cotal meshes add`, a `--server` override, a `cotal supervise`\nprocess still running from any folder whose command line names that space or no space, or a broker\naddress where anything accepts the connection or the dial does not finish in time. With several\nmeshes running and no `--space`, `--server` or `--creds`, the install is machine-wide, so every\nrunning manager is reported in turn, each under its space name, before anything is written; a\n`legacy` verdict on any of them makes the whole run not a hot update. A selector flag still reports\none manager. A manager without a\ncustody generation is reported as `legacy`: it cannot preserve its manager-owned PTYs, so the\ncommand says that this is not a hot update and prints `exact`, `fork`, `fresh`, or `drain-only`\nfor every seat. This report sends no stop, preservation-commit, or replacement command.\nIt does not preserve a running PTY on a legacy manager. The built-in pty runtime spawns\nin-process on every platform and reports `legacy`. On Linux it still adopts seats that an earlier\nmanager left under a detached custodian, but it starts no new custodian. An incompatible native\n`@lydell/node-pty` or ConPTY ABI break remains an explicit per-seat maintenance cut.\n\nWith `--self`, the selected running manager is reported before any global install. When a newer\nrelease exists, Cotal then installs the exact version it validated, resolves and verifies that\npackage in npm's global root, then launches that binary with the same `--space` / `--server` /\n`--creds` selection to reconcile connectors and first-party extensions to the new generation. An npx\nor dev-clone invocation therefore installs and continues through a separate global copy; it never\nclaims the already-running process changed. From a source checkout, the `update --self` hint in\n`update` and `update --check` says so. If the binary is current, `--self` performs the normal\nlocal reconcile without reinstalling it. Each npm install it runs starts with 022 added to your\numask, so a umask of 0002 does not leave the installed tree group-writable.\n\nThird-party extensions are listed with their installed version and recorded spec but are not\nauto-updated in v1. Floating third-party updates require `@cotal-ai/*` peer-range validation and are\na future follow-up. A failed connector/extension install, npm metadata check, or requested global\ninstall is reported and makes the command exit nonzero. Independent extension attempts continue so\nthe output includes every failure; an unavailable npm registry does not undo a completed local\nreconcile, but the command still exits nonzero because it could not establish that the install is\ncurrent.\n\n## up\n\n```bash\ncotal up [--foreground|--detach] [--open] [--space <s>] [--root <prepared-dir>] [--server <url>] [--channels <path>] [--runtime <name>]\ncotal up --user-auth --idp <url> [--exchange-public-port <n> --exchange-public-url <https://\u2026> [--exchange-trusted-proxy]]\n [--advertised-server <url>] [--agent-provisioning-url <https://\u2026>]\ncotal up --tls-cert <cert.pem> --tls-key <key.pem> # serve broker TLS (both, or neither)\ncotal up --restore <dir> [--restore-only registry] [--accept-missing-source]\ncotal up -f <cotal.yaml> [--dry-run] [--runtime <name>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--server <url>` | auto (free local port) | Listen URL override: one `nats://` or `tls://` URL with a host, without credentials, path, query or fragment. Any other value refuses `USAGE_INVALID` before anything is written. A controller stack's broker also needs one IP literal that is not a wildcard, which `up` checks before it writes the stack journal. With `-f`, it overrides the manifest's `broker.servers` and follows that field's rules instead: a valid URL with no embedded credentials |\n| `--host <host>` | none | Bind host override for a **fresh** broker boot: an IP or hostname only, never a URL (that is `--server`) and never `host:port` (the port comes from `--server` or its default); a URL or port-bearing value is refused pointing at the right flag. With no `--server`, the broker URL is derived from it, so `--host <addr>` alone is enough to make a mesh reachable at that address; a `--host`/`--server` pair naming different addresses is refused. A wildcard bind (`0.0.0.0`, `::`) keeps a dialable loopback URL. Recorded on the mesh and reused by every later manager launch, so a repair or resume keeps remote [`attach`](#managed-seats) working. A live refresh (`\u2713 mesh already running`) does not rewrite `.cotal/auth/server.conf` or rebind nats; stop the broker, then re-run `up --host` |\n| `--space <s>` | the folder's space | Space name |\n| `--root <prepared-dir>` | current folder's workspace | Use an existing local project's own `.cotal` root rather than inheriting one from the invoking folder. An absent, inherited or symlinked `.cotal`, or the machine's home store, refuses before startup. Prepare a new folder with `space create <space> --root <dir>` first. Relative file, restore, channels, TLS and store paths remain relative to the invoking folder. The native `up` selection rule still applies after startup. |\n| `--store-dir <dir>` | none | JetStream store directory (recorded; a repair up reuses it) |\n| `--max-file-store <bytes>` | nats-server's dynamic cap | JetStream file storage cap in bytes (a positive integer, no unit suffix). Without it nats-server sizes the store at start as three quarters of the free space on its filesystem. The cap is fixed at broker start: a running broker cannot change it (`cotal down` first), `down --preserve-state` keeps it for the resume, and a resume with a different value is refused. Not accepted with `-f` |\n| `--channels <path>` | `.cotal/channels.json` if present | Channel-registry seed file (JSON). An explicit path that is missing is an error |\n| `--restore <dir>` | none | Restore a completed offline backup before exposing the normal listener |\n| `--restore-only registry` | artifact selection | Restore only the registry component |\n| `--accept-missing-source` | off | Explicit disaster consent when the inode-bound preserved source is absent |\n| `--accept-stale-checkpoint` | off | Explicit consent to resume a seat whose checkpoint was captured outside its recorded recency horizon |\n| `--open` | off (auth) | Unauthenticated dev mesh: no JWT, no ACLs |\n| `--user-auth` | off | Per-user auth: people `cotal login`; connects are authorized against the actor ledger |\n| `--idp <url>` | none | With `--user-auth`: the IdP auth base URL to pin on first enable. A later run may omit it or pass any spelling that normalizes to the pinned URL, such as one with a trailing slash; a different IdP is refused. With `-f`, overrides the manifest's `broker.idp` under the same rules: https (or http on a loopback IP literal), with no credentials, query or fragment |\n| `--exchange-public-port <n>` | none | With `--user-auth`: add the public exchange face on this loopback port, for an HTTPS reverse proxy to forward to |\n| `--exchange-public-url <https://\u2026>` | none | With `--exchange-public-port`: advertise the reverse proxy's HTTPS URL in discovery |\n| `--exchange-trusted-proxy` | off | With `--exchange-public-port`: attribute public failure buckets to the last `X-Forwarded-For` hop. Enable only when the listener is reachable solely through a trusted proxy; otherwise the socket address is used |\n| `--advertised-server <url>` | `--server` | With `--exchange-public-port`: the broker address the public discovery bundle tells participants to dial, such as a `wss://` address behind your proxy. Must be a `nats://`, `tls://`, `ws://` or `wss://` URL |\n| `--agent-provisioning-url <https://\u2026>` | none | With `--exchange-public-port`: the deployment's agent-provisioning endpoint the public bundle advertises. A participant's `cotal spawn` on a remote user-auth mesh sends its login bearer there to provision the agent; without one, that spawn needs a one-time enrollment ([Run a mesh](run-a-mesh.md#remote-supervised-seats-by-enrollment)). HTTPS only |\n| `--service-endpoints` | off | With `--user-auth`: the auth service also hosts service endpoints: the public registration door and the authenticated host route that `cotal service-endpoint` drives |\n| `--detach` | on | Run in the background, which is also the no-flag default |\n| `--foreground` | off | Keep native stack lifetime in this terminal. Required when a systemd/launchd unit supervises `up` itself. Cannot combine with `--detach` |\n| `--tls-cert <path>` | none | PEM certificate to serve TLS with. Must be given together with `--tls-key`. Before starting the broker, Cotal checks readability, private-key mode, key/certificate match, the validity window, and host coverage. `nats-server` accepts an expired certificate and leaves the failure to clients, so Cotal performs these checks first. The decision is recorded; a later bare `cotal up` keeps serving TLS |\n| `--tls-key <path>` | none | PEM private key for `--tls-cert`. Refused if group- or other-readable (tighten to `600`) |\n| `--file <cotal.yaml>`, `-f` | none | Launch a whole mesh from a manifest |\n| `--dry-run` | off | With `-f`: print the plan, mutate nothing. A preview from an unprepared folder under the machine home reads that folder's empty policy rather than inheriting the home store; applying there requires `space create` first. |\n| `--runtime <name>` | `pty` (or the manifest's, with `-f`) | Agent runtime for the mesh manager (`pty` built in; others are installed extensions, explicit-only). Resolved + probed before the broker starts; an uninstalled/unreachable runtime fails loud. Without `-f`, that refusal is `DEPENDENCY_UNAVAILABLE` (exit 2) before anything is written, and for a known runtime the remedy names the `cotal ext add <pkg>` to run. With `-f`, overrides the manifest's runtime |\n| `--max-sessions <n>` | 64 | Live-session ceiling for the mesh manager. Each console pane and each `cotal attach` is one session, so size for agents \xD7 panes, not agent count. Recorded on the mesh and reused by every later manager launch, so a repair or resume does not silently drop back to 64. A running manager cannot change it: `cotal down` first, then `cotal up --max-sessions <n>` |\n| `--no-manager` | off | Broker-only boot: start the broker and, in auth mode, the delivery daemon, and no local manager. A refresh under the flag of a mesh whose manager is live refuses rather than keeping or stopping it. For a controller stack, use `cotal service stop stack-manager --space <s> --yes` first. A legacy stack has no command that stops only its manager: `cotal down` stops the whole stack, then `cotal up --no-manager --space <s>` starts it broker-only. Cannot be combined with `--runtime`, `--max-sessions`, or an agent-declaring manifest |\n| `--rotate-sys` | off | Rotate the space's system account and re-mint its two `$SYS` creds. Needs a stopped mesh; refused with `--open` |\n\n`cotal up` boots a local nats-server with JetStream and, in auth mode (the default), JWT auth and\nper-agent ACLs. It runs in the background by default and records the mesh so `cotal spawn` from\nany directory can find it; `--detach` explicitly names the same mode. `--foreground` keeps the\nnative stack in this terminal. A contradictory mode pair refuses before setup or seed work. An\nunqualified `up` from a folder that inherits the machine's home `.cotal` refuses before seeding:\nprepare that folder with `space create <space> --root .`, then run `up --root .`, or move to the\nhome folder if the home stack was intended. When `--root` names another prepared project, run bare\n`cotal down` from that project folder: bare down\nstops the folder's stack, and `down --space` is not a remote replacement for that scope. With no\n`--space`, `up` runs the folder's space: the one its auth account names, else the one its stack or\nrunning mesh records, else the one prepared there, else `main`. A second `up` in the same folder\nruns the same space again. A folder whose stack and registrations name more than one space refuses\nwith `CONFLICT` and asks for `--space`. With no\n`--server`, it auto-selects a free port if the default address is taken, and a later `cotal up` of\nthe same space from the same folder starts from the URL that mesh recorded; an explicit `--server`\nstays fail-loud on collision. A background boot also brings up the control plane (delivery daemon in auth\nmode, then the manager). `--no-manager` is the broker-only mode: it boots\nthe broker (and the delivery daemon in auth mode) and starts no manager, so there is no manager\npidfile to leave stale. A refresh under the flag of a mesh whose manager is live refuses rather\nthan keeping or stopping it. Stop a controller stack's manager first with\n`cotal service stop stack-manager --space <s> --yes`. A legacy stack has no command that stops only\nits manager: `cotal down` stops the whole stack, then `cotal up --no-manager --space <s>` starts it\nbroker-only. For a split topology with a manager, wait for `.cotal/manager.<spaceKey>.log` to contain `\u2713 manager up`, then stop that manager the same way on that\nhost and run [`supervise`](#supervise) against the remote broker; see\n[Run a mesh](run-a-mesh.md). `cotal up --detach` prints `\u2713 running in the background:` with\n`manager` listed (pidfile liveness, not a teardown boundary); with `--no-manager` the line lists\nonly what actually started. Started from the machine home, the line names `cotal down --preserve-state`,\nbecause bare `down` refuses the home stack. With `--no-manager` it names `cotal up` first, because\n`--preserve-state` needs a manager. On POSIX, a foreground broker has its own session and process group.\nTerminal Ctrl-C reaches the CLI owner, which stops the manager through the same stop as bare\n`cotal down` (see [`down`](#down)), then the rest of the stack, and reports managed agents under\nthe same rule. When the manager stop is refused, Ctrl-C prints the refusal with the reap route and\nleaves the stack running, including the manager, broker, delivery and any auth service. A second\ninterrupt during teardown does not start another stop. Windows keeps its existing console signal\nbehavior. The `-f` form is a\n[manifest deploy](#manifest-deploys).\n\nAn `up` that starts or refreshes a space on this machine writes the `default` persona into\n`<root>/.cotal/agents` when the root has none, so a bare `cotal spawn` has a persona to launch. A\nspace prepared or started before it had one gets it on its next `up`, and a persona you edited is\nleft as it is. A controller stack lists the write under `result.written` and in its report, and the\nretained launcher prints the path it wrote. If the write fails, the stack stays up and the failure\nis a warning. A controller recovery or restore leaves the persona to the next `up`.\n\nA space name belongs to one registered mesh. `up` refuses a name that another live mesh holds. It\ntakes over a name whose broker is not running, and when that registration belonged to another root\nit prints that root and its broker address. It refuses instead when that root holds a restore or a\npreserved cut in its maintenance journal: finish that attempt from its root, or start this stack\nunder another `--space`. It also keeps the name, and names that root, when the root or its journal\ncannot be read, for example when access to a parent folder is denied, or when the recorded root is\nnot an absolute path. A recorded root matches this one only by the folder it resolves to on disk, so\na path such as `<this root>/link/..` whose link leads through an unreadable folder keeps its name\nlike any root that cannot be read. A root counts as gone only when it no longer exists or is not a\ndirectory. A readable root with no maintenance journal also gives up its name. Each of these\nrefusals ends `refused` with exit 2, on a controller stack and on the retained launcher, and shows\nits reason and the next step, such as `cotal meshes rm <space>` for a root that is not an absolute\npath.\n\nA foreground `up` holds the root's maintenance lock only while it starts the stack. A bare\n`cotal down` from another terminal can then stop the stack; the foreground `up` sees the completed\nstop or preserve, prints `Foreground stack released` and exits 0. A systemd unit that runs\n`up --foreground` should set `KillMode=mixed`, so the stop signal reaches only `cotal up`, which runs\nthe guarded stack stop, and a `TimeoutStopSec` long enough for that stop. The default\n`control-group` mode signals every member at the same time, and a manager signalled directly does\nnot stop through its request-bound stop.\n\nThe generated broker configuration is owner-private (`0600`) in open and authenticated modes,\neven with a group-writable umask. `up` creates `.cotal` and the broker store owner-private (`0700`)\ntoo. An existing directory keeps its mode. The broker log `.cotal/nats.log` and the auth service,\ndelivery and manager logs are created owner-private as well, and the process records `up` writes, such as\n`.cotal/nats.pid` and its identity pin, are created `0600` under a fresh name that is never opened\nthrough a symlink, then renamed into place. The pid record and identity pin of a native service\ninstance are published the same way. A file Cotal writes in place under `~/.cotal` or a\nproject's `.cotal`, such as a generated configuration, a credential, a process marker, a persona,\na seat record or one of those logs, is never written through a symlink.\nA file you own is set to `0600` through the open file. A symlink, a file another user owns or a name\nthat is not a regular file is refused and left unchanged. A private directory is set to `0700`\nthrough its path, which follows a symlink; this step does not refuse a symlinked `~/.cotal` or\n`.cotal`.\nOn Windows the owner and symlink checks do not\napply, because POSIX modes are not that platform's protection model; the file is opened with mode\n`0600` and private secrets are locked down by their access list. Local records under `~/.cotal`\nand a project's `.cotal`, such as the stack journal, service records and deployment attempts, use\nthe same model there. A store directory Cotal creates gets the private access list, and a record is\nwritten like a private secret. Reading a record or checking its path does not check an owner, a mode\nor an access list, and does not refuse a link: a symlink or junction is followed. Size limits, the\nrecord schema, the regular-file check and the refusal of a link inside a record store's own\ndirectories still apply. When a service path check refuses a path, the refusal names the option,\nthe path that failed, and its owner and mode. A whole-stack failure shows that sentence as its\nnative detail.\n\nA fresh prepared static-auth, open or plain-loopback user-auth root starts through the service\ncontroller. It records its mode, definitions and full instance IDs in the private\n`.cotal/stack-controller.json` journal. Static-auth startup prepares trust and configuration, starts\nthe broker, initializes native streams, then starts delivery and the optional manager. Open startup\nprepares an unauthenticated broker and streams, then the optional manager; it creates no delivery\nservice or credential. Each member reports its own readiness; the result does not prove agent or\ntask readiness. `--json` returns one\nstack receipt and `--plain` returns a line-oriented report. Partial startup keeps confirmed members\nand reports them; unknown operations are retained and never retried automatically. A failure while\ninitializing streams or recording the broker registration is attributed to the broker, not the next\nunstarted member. The failure target names that stage. Typed JetStream API errors retain their\nnumeric code and status without copying the server's description or changing the unknown outcome.\n\nA journal root always keeps that controller route and its recorded mode and broker address, even\nwhen a completed stop removed its mesh registration. Bare `up` reuses that address; an explicit\nconflicting `--server` or `--open` refuses before effects. Legacy-only options refuse rather than switching launchers.\n`up -f`, recovery and existing legacy roots retain their native paths; those paths refuse the\nnew JSON/plain flags. A legacy root is one with no stack journal that is neither prepared nor\nregistered for the controller, such as a folder an earlier release started and a normal `down` then\nunregistered. Plain `cotal up` there starts it again on its retained launcher, with its identity,\nstore and history. `space create` in such a folder refuses `CONFLICT` and names that `cotal up`.\nA controller `up` checks that the space is prepared or locally started at its root before it writes\ntrust, configuration, a definition, an instance or the journal; a refusal there leaves the folder as\nit was. When a completed stop and `space forget` removed that record,\n`cotal space create <space> --root <root>` prepares the same space at its root again, and `up`\nstarts it from its journal. Schema apply is not supported. Fresh `team boot` uses the controller\nroute described below. Recovery re-entry keeps its recorded mode.\n\nA whole-stack `up` or `down` takes the root's maintenance lock before it reads the journal. While\nanother whole-stack or maintenance command holds it, the command refuses `LIFECYCLE_BLOCKED` and\nnames that command's pid. A crashed member, or an `up` or `down` killed before it recorded its\noutcome, is settled by the next bare `up` or `down` from process evidence. A member whose recorded\nprocess (pid and start time) is gone is recorded as failed, the earlier operation is recorded as\nfailed, and the command continues with an `OPERATION_SETTLED` warning. A member that is still\nrunning is kept: `up` reuses it and `down` stops it. A member whose process cannot be established,\nor one with a stop that was requested and never confirmed, refuses `LIFECYCLE_BLOCKED` and names\n`cotal service status <definition> --instance <id> --space <space>`. A manager stop whose request\nwas published and not confirmed before `down` stopped waiting keeps the manager's record `stopping`\nunder that request, also after its process exits. The next bare `up` or `down` reads that request's\nown receipts first. When the manager recorded the stop as completed and its exact process is gone,\nthe record becomes `stopped` with that receipt, the request's reservation is released once the\n`down` that took it has exited, and the command continues. When the manager's only receipt is a\nrefusal, which it writes when it started no stop, for example because that `down` had already\nexited, the request is removed, its reservation is released, and the record returns to what its\nprocess shows, so `down` stops a running manager and `up` reuses it. Otherwise it refuses\n`LIFECYCLE_BLOCKED` and names the instance and the original request. `down --dry-run` writes\nnothing, so it does not settle such a stop and refuses the same way. `service start`, `restart` and\n`stop` refuse it `LIFECYCLE_BLOCKED`, exit 2, before any readiness probe, stop request or record write.\nThe earlier stop's outcome stays unknown. When the launch's\nrequest file does not read as its own, the refusal reports that it is unknown whether a stop was\npublished. A manager record that an earlier release left `unknown` while its launch still holds a\nstop request is recorded `stopping` under that request when its process is gone. Its exit is never\nrecorded as a crash, and no `up`, `down` or `service` command relaunches it, removes its record or\nremoves the stack's registration. A stop that has neither a completed nor a refused receipt, such\nas one whose manager was killed before it read the request, is cleared only on request:\n`cotal down --abandon-stop <request-id> --yes`, with the request ID the refusal names. It refuses\n`LIFECYCLE_BLOCKED` while the manager or the `down` that sent the request still runs, and\n`TARGET_NOT_FOUND` when no member retains that request; either way nothing changes. Otherwise it\nremoves the request, the stop reservation and a `--with-agents` shutdown intent that the same stop\narmed, records the manager `failed`, and stops the rest of the stack with a `STOP_ABANDONED` warning.\nWhether that manager stopped or released its agents' seats is not confirmed, and a seat that runs\noutside the manager process may still run. It needs `--yes` and refuses `--dry-run` and\n`--with-agents`. When `up`\nredefines a member because its launch options changed, the journal records the\nreplacement's instance and launch request before the old instance is retired, and the new\ndefinition's revision before the replacement launches. After an `up` cut off in between, the next\nbare `up` or `down` adopts the replacement only when its record carries that launch request and\nrevision for the same operation; `down` stops it and `up` reuses it. Any other instance record\nrefuses `CONFLICT`, and nothing is adopted. A preserve or resume that\nnever recorded its outcome stays with its own maintenance attempt. A member that exits before\nreadiness is recorded as failed with its exit code, and the refusal names its log with\n`cotal service logs <definition> --instance <id> --space <space>`.\n\nBefore it launches a broker, the controller binds the recorded broker address once. When another\nprocess holds it, `up` refuses `CONFLICT`, names the address and the bind error, and launches\nnothing; the stack keeps its recorded address. A text `up` prints one `Starting <definition>` line\non stderr before each member it starts, with the readiness timeout it waits for. A started stack's\nNext offers `cotal spawn`, or `cotal up` to add the manager after `--no-manager`, and `cotal down`.\n\nOn a controller root, `down --preserve-state` refuses `UNSUPPORTED` (exit 2) in this release, in\nevery stack mode. It refuses while validating its arguments, before the confirmation and before any\nstop, write or manager request. Plain `down` stops the stack and keeps its store, channels and\nhistory for the next `up`. Preserving a controller-owned stack returns in a later release.\n`backup create`, `backup restore` and `up --restore` need a preserved cut, so they refuse the same\nway on a controller root (see [Backups](#backups)).\n\nUser-auth controller startup requires a plain loopback IP and an auth provider with read-only\nserver preflight. A first enable checks the required IdP and its JWKS before creating maintenance\nstate, a stack journal, trust or credentials. Missing or invalid inputs are usage refusals; an\nunavailable JWKS is a dependency refusal. Malformed IdP inputs are not echoed. An explicit `--idp`\nis also compared with the recorded pin before maintenance state changes, even while the broker is\nrunning. A different IdP refuses without refreshing members or rewriting their journal. The controller\nchecks again under its lock, and the provider repeats its authoritative checks during preparation,\nbefore any member launches. Later races or failures retain their records and are not rolled back.\nIt starts the broker, initializes streams, records provisional public trust metadata, then starts\nthe `stack-auth` service. Only that exact process's provider readiness supplies the final endpoints.\nDelivery uses the same hosting-root authority as native startup. No user caller is replaced by a\nstatic or borrowed caller.\n\nThe manager requires an existing signed-in, granted CLI login. First enable can therefore start\nthe other members and return partial `OPERATION_PARTIAL` with native cause `AUTH_REQUIRED`, while\nleaving the manager unattempted. Sign in with `cotal login`, obtain the required grant, then run\nbare `cotal up` to continue that known partial operation. `--no-manager` omits that member and its\nlogin requirement. Unknown outcomes are never continued or replayed. Public exchange faces and\nTLS-required or remote user-auth brokers still require the retained native route.\n\nThe journal reads earlier three-member auth/open records with an unattempted auth-service row.\nIts next write records all four roles. It neither adopts legacy processes nor converts a mode.\n\nA repair `up` on a mesh whose broker died reopens the store its record names, and refuses a\ndifferent `--store-dir` rather than silently opening a second store.\n\nThe generated `.cotal/auth/server.conf` is written on a real broker boot and is not an\noperator-owned config. `--host` changes that file only when nats is actually started. A unit\nrestart that leaves an answering listener in place is a refresh, not a rebind.\n\nOn an existing mesh, `cotal up` reconciles the presence and lease bucket TTLs. It writes a reserved\ncanary and waits for the bucket to expire it before reporting success. If the broker accepts the\nstream update but the backing store does not persist or enforce it, `up` exits nonzero with a TTL\npersistence error instead of trusting the value returned by stream info. A refresh that restores a\nmissing manager waits for that process to come up, then says so with its pid (`\u2713 restored in the\nbackground: manager (pid N)`). A manager that `up` starts and that exits at boot instead is reported\nas `! control plane degraded:` with the path of its log, and `up` then exits nonzero, whether it was a\nrefresh, a first `--detach` boot or `up -f`. The same happens when another\n`cotal supervise` takes over the manager pidfile before the started process comes up. A legacy\nrefresh that finds everything already running prints only the `\u2713 mesh \"<space>\" already running`\nline. A controller refresh prints its members under `Controller stack already running`, and its JSON\nresult is a no-op unless it wrote the default persona. Neither lists a role the stack's mode does not use, such as auth on a\nstatic-auth stack. A first\nboot starts its manager without the restore line.\n\nA manager or delivery daemon that is killed, or a daemon that exits because its broker went away,\ncannot release its lease. The row stays until the bucket TTL removes it: 10s for the manager and\n30s for delivery. A repair `up` inside that window waits out the row when it can show the holder is\ngone, then starts the replacement, so one run restores the control plane. For the manager, the row\nmust name this workspace root and a pid that is not running on this host; the new manager logs\n`which is not running on this host; waiting` and the seconds left. For delivery, no daemon may be\nrecorded running in this folder and the row must be held by the credential this folder gave its\nprevious daemon; `up` prints `shard 0 is still leased to this folder's previous delivery daemon`.\nThe wait never writes the lease. A row that is written again while `up` waits has a live holder, so\nthe start is refused and `up` exits nonzero. A row held by any other process is refused at once,\nas before.\n\nA delivery daemon that `up` launched and that exits before it is ready fails the start. The error\nquotes the failure the daemon logged during this launch and names its log,\n`.cotal/delivery.<spaceKey>.log`. It describes a lease held elsewhere only when the daemon logged\nno failure. A daemon start that fails on a root whose maintenance journal holds a restore or a\npreserved cut never suggests `clean store`, because that store holds the restored or preserved\ndata. Check the root with `cotal status` first.\n\nA full restore recreates the delivery daemon's durables from their checkpoints, at their recorded\nstart sequence. The daemon binds such a restored durable when it otherwise matches its own\nconfiguration, so a restored store starts. A durable that differs in any other setting still\nrefuses.\n\n\n`--user-auth --idp <url>` starts the space's auth service alongside the broker: the NATS\nauth callout plus its capability-gated local exchange, and optionally the closed public exchange\nface configured by the three `--exchange-*` flags above. The service is torn down with `cotal down`,\nand a re-run of `cotal up` heals a dead service on a running broker. `up` waits for the service to\nfinish binding: while the daemon it launched (or found running) stays alive, the wait extends past\nthe base 15s up to 60s; a daemon that exits is refused at once with \"exited before becoming ready\",\nand one alive past 60s is refused as \"alive and still starting\" (wedged), naming the pid record and\nthe service log. `--user-auth` and `--open`\ncontradict each other and are refused loudly; a running broker cannot change auth mode\nwithout a `cotal down` first. See [identity & auth](identity-and-auth.md).\n\n`--rotate-sys` renews the two `$SYS` credentials (`membership-observer`, `connection-evictor`).\nThey carry a 30-day expiry and nothing re-signs them in place, because the trust record never\ncarries the system-account seed, so they are renewed by issuing a **new system account** under the\nsame broker operator and minting fresh creds against it. The root keeps that seed in its own secret\nstore, written at space creation and replaced here, so an auth service started with\n`--service-endpoints` can mint the short-lived observer credentials service caller issuance needs. A plain re-`up` does **not** do this: it reuses the\nexisting trust record, and its `$SYS` creds along with it.\n\nThe rotation is safe to run on a real space, with one operational cost. The data account, the account\nsigning key, every agent credential minted from it, and the JetStream store are all untouched; what\ndies is the retired system account, and with it any out-of-band copy of the old `$SYS` creds, on every\nbroker that loads the rotated config. The cost is that **earlier full backups stop being restorable**\n(see below), so this is not a no-consequence operation. It needs the broker to restart on the rewritten\nconfig, so it runs as part of a boot:\n\n```bash\ncotal down\ncotal up --rotate-sys --detach # agents reconnect; nothing is re-provisioned\ncotal doctor auth # both $SYS creds healthy again, 30 days out\n```\n\nA rotation is a stopped, fresh boot, and anything that is not one refuses it, all for the same reason\n(the on-disk material and the broker it runs on must never end up on different generations):\n\n- a live mesh, because the running broker would keep serving the retired account;\n- an open mesh, whether that comes from `--open` or from `broker.auth: false` in a manifest, which\n has no system account at all;\n- `--restore`, because reinstating a trust root and superseding it in one command leaves no way to\n say which authority the mesh came up on;\n- an unfinished restore or resume attempt on this root, including one `cotal up` would recover on\n its own, because those paths can adopt a live listener and return without booting a broker;\n- a root that hosts more than one space, because the system account lives in the shared broker\n record and a rotation would retire every tenant's, while the root holds one `$SYS` cred pair\n pinned to one data account.\n\nTwo things to know before you run it:\n\n- **The retirement is config-load-bound.** Old `$SYS` creds are refused by any broker that loads the\n rotated config. A stale `nats-server` still running the *previous* config in memory would keep\n honouring them, so stop every broker for this root first. `--rotate-sys` refuses if this root's\n mesh is recorded as running, if anything unidentified is answering at the address it was given, or\n if the root's pid file names a live (or unreadable) process. Those are Cotal's own ownership\n records, not a scan of the process table: a `nats-server` you started by hand against this root's\n `server.conf` on some other port writes none of them and will not be seen. Do not run one.\n- **It invalidates earlier full backups.** A full artifact binds to the trust chain it was taken\n against, and that commitment covers the operator JWT and the system account. Every full backup\n taken before a rotation refuses to restore afterwards, so take a fresh `cotal backup` once the\n rotated mesh is up. `cotal up --restore` names this case when the data account still matches.\n\nThe commit is not atomic (a trust-record write plus two credential writes), so an interrupted\nrotation leaves the record ahead of the creds. That split is detected rather than silent: every\n`cotal up` on an auth mesh, and every `cotal doctor auth`, compares each `$SYS` cred's issuer against\nthe persisted record and names the retired account. `up` warns rather than refusing, because these\ncreds power the membership graph and live eviction, both of which degrade fail-soft; the mesh is not\nworth taking down over them. Re-running the rotation heals it, at the cost of one generation.\n\nWhile those creds are expired the mesh keeps delivering messages, but the\n[membership feed](delivery-daemon.md) and live connection eviction stay down; `cotal doctor auth`\nand the manager's log both name the credential and this repair.\n\n## down\n\n`down <service-name>` routes one saved owned definition through the\nsame selection, confirmation, reservation, native manager-impact and stop controller as\n`service stop`. Use `--instance <full-id>` when selection needs a full instance pin. `--yes`,\n`--dry-run`, `--space`, `--json` and `--plain` keep that controller's meanings; structured receipts\nname `down` while retaining the service-stop result and actual identity. Unknown definitions do not\nfall back to legacy component pidfiles. A controller stack names its members `stack-broker`,\n`stack-auth`, `stack-delivery` and `stack-manager`; a legacy name such as `manager` refuses\n`TARGET_NOT_FOUND` there and names the `stack-` member to use. Completion offers saved definitions without imports or a\nnetwork request. No hidden connector repair or shared catalog preparation precedes this route.\n\nMultiple names resolve as one selected-space set. All owned targets and process roles are checked\nbefore effects. After confirmation, the controller reserves every definition in stable order and\nrechecks all records and native impact before the first stop. Stops follow the displayed input\norder, with each native guard rechecking at effect. A later failure stops the sequence; one receipt\nretains completed, failed/unknown and unattempted members. Reservation/preflight refusals also\nretain the full selected set. Release is attempted for every acquired definition hold; a reported\nrelease error makes an otherwise known outcome partial without erasing known stop results. An\nalready unknown stop stays unknown. Lock cleanup is not independently proved. This is not atomic and no rollback or cleanup is claimed. Dry-run reads metadata without reservations, provider imports or native impact\nqueries. Duplicate names and `--instance` with multiple names are usage errors. Named `--with-agents` refuses and points to\n`cotal service stop <manager-name> --instance <full-id> --with-agents`; it is never dropped.\n\nBare `down --dry-run` reads local process records for one registered space at the current\nproject root. A root with no `.cotal/` directory, or with no stack registered at it, refuses\n`TARGET_NOT_FOUND`, exit 2, because there is no stack there to preview. A root whose stack is\nalready stopped shows the no-op described below instead. Add `--space <name>` to\nconfirm that exact space belongs to this root;\n`--json` and `--plain` produce a partial, finite inspection receipt. It lists pidfile\nliveness, readable additive deployment IDs, retained additive attempts and\nunreadable paths under this root, and saved service definitions, instance IDs, recorded states and\nhosts, supervisor receipts, and retained install, uninstall and unit-action\nattempts in the selected space's workstation store. Saved records are not attributed to this project root, and their recorded\nstates do not prove live process or supervisor status. A submitted native attempt may have\nchanged state; the preview neither reconciles nor retries it. For each saved service instance it\nalso shows what bare `down` would do with it: stop it, skip it as already stopped, or refuse, with\nthe reason. That plan observes processes on this host and changes nothing. Other local registrations with\nthe exact recorded broker address appear as possible sharing. No match does not exclude\naddress aliases, remote tenants or live attachments. The preview does not establish process\nownership, the complete service set, exclusive broker use or agent impact.\n`--with-agents` only marks the requested policy. It does not reserve or signal\na process, and the receipt is not an authorized stop plan. A fresh `team show`\nsummary points to this read-only command from the recorded root.\n\nOn a root with a controller journal, bare `down` reads only its recorded full instance IDs. It\nshows the member set and the live manager's native agent impact before confirmation, then stops\nmanager, delivery and broker in that order. `--yes` acknowledges that scope in scripts; `--with-agents`\nrequests the manager's native retirement policy. The broker scope is checked again after its\ndependants stop. A failure stops the sequence and preserves completed and unattempted members.\nOnly a complete confirmed stop removes the delivery credential and local registration. Saved\ndefinitions and the journal remain for the next `up`. No descendant, lease or authority cleanup is\nimplied. A dry run reads records only, takes no lock, does not establish native impact and exits 0;\nits JSON marks `completeness` as `partial`. It lists members in stop order, says when the stop would\nfirst settle an earlier operation, and names a scope check the real stop would refuse as a warning.\nA member whose recorded process is already gone is recorded as stopped without loading its adapter\nor credential, so a second `down --yes` stops nothing and is a no-op. When the manager is\nlive but the broker is not, `down` cannot read the manager's agent impact and refuses before any\nstop, from a managed seat's shell too. Run `cotal up --space <space>` to restart the broker from its recorded inputs while the\nmanager keeps running, then run `cotal down`. Controller roots refuse\npreservation and manifest flags rather than switching to the legacy launcher. Outside a terminal,\na bare `down` that would stop a running member refuses without `--yes`, exit 1, and prints that\nline and its remedy without the command help.\n\nA bare `down` in a folder whose stack is already stopped stops nothing. It reports that the stack\nin that folder is not running and names `cotal up`, outcome `no-op`, exit 0, and writes nothing,\nso it needs no `--yes`. `--dry-run`, `--json` and `--plain` show the same. A controller stack\ncounts as stopped when its last whole-stack operation was a completed `down` and no member and no\nother service instance of its space runs. That check reads no process record under the root; those\nare read only as described below. A stack that stopped any other way takes the full stop, which\nalso finishes its cleanup. A legacy root counts as\nstopped when no registration names it and no `*.pid` record remains at the top of its `.cotal`,\nand the root still holds the broker configuration a start rendered there or a space prepared there.\nWithout that evidence a legacy root still refuses, with one next step: `TARGET_NOT_FOUND` when\nnothing shows a stack there, and `CONFLICT` when several registrations name the root or a process\nrecord remains that no registration explains. Before either kind of root reports the no-op, `down`\nreads every record a local-process descriptor names under the root, an installed extension's\nincluded, at any path and name under `.cotal`. A descriptor whose record name holds a space is read\nfor every space its files under the root name, as well as the stack's own. A record whose process\nruns, whose liveness the kernel does not answer, or that cannot be read or attributed refuses\n`LIFECYCLE_BLOCKED`, and so does a file whose space part decodes to no space. Nothing outside the\nroot's `.cotal` is read: a link anywhere on a record's path refuses at the link, unread, as does a\nrecord that is not a regular file, is larger than a process record, cannot be opened, or changes\nwhile it is read. Each is named once, at the path where reading stopped. A `.cotal` that is itself\na link never gives the no-op: on a stopped controller stack `down` refuses at `.cotal`, and on a\nlegacy root it refuses `CONFLICT` before any record is read. A template whose `{space}`\nnormalizes away names one path for every space and is read as that path. The refusal lists each\nrecord by name, path under the root and pid, and its next step names the pids and paths to check.\nIt stops, adopts and removes nothing. `--with-agents` on a stopped stack refuses\n`LIFECYCLE_BLOCKED`, because no running manager can stop the agents; start the stack first. Its\npreview, `cotal down --dry-run --with-agents`, shows the no-op.\n\nThe legacy bare-stack and manifest apply paths described below are retained. They do not\naccept the new named-service JSON/plain/yes apply receipt flags, except that `--json` and\n`--plain` show the no-op of a stack that is already stopped. A bare stop of such a root needs\nno `--yes`, so `cotal down --yes` there is refused and stops nothing; run `cotal down` without it.\nBefore a retained bare apply\nreaches its pidfile sweep, it now requires one locally started registration under\na real isolated project root and refuses pending maintenance transitions, any valid or\nunreadable additive ledger or attempt under that root, or any other local registration at\nthe exact recorded broker address. It reads each record its sweep stops as the stopped-stack check\nabove does. A record that check leaves unread, such as one behind a link, refuses `LIFECYCLE_BLOCKED`\nbefore the first stop and is named once, where reading stopped. No process behind it is read or\nsignalled, and no record is removed. These are known-blocker\nchecks, not a complete live broker-sharing or stop plan; aliases, remote tenants,\nagent impact and process identity remain unproved. These paths do not check full\nwhole-stack target ownership, set exclusion, confirmed agent impact or schema\nreconciliation. The retained `down --preserve-state`\nroute separately refuses a retained or unreadable additive record under its\nmaintenance lock, without adopting its effects into the preserved set.\nA validated completed restore in `active` for the registered space does not block ordinary\nbare `down`. The other root, ownership, sharing and native process guards still apply, and\nstopping leaves the restore journal and fallback store unchanged.\nThe machine home is not an isolated project root, so bare `down` refuses the home stack. Stop it\nwith `cotal down --preserve-state`, which keeps its state for the next `cotal up`. The refusal names\nthat command. `--preserve-state` takes its agent inventory from the manager that serves the space.\nWhen no manager holds the space's lease, as on a stack started with `--no-manager`, it refuses\n`TARGET_UNAVAILABLE`, exit 2, before it stops anything or writes a maintenance record. Start the\nmanager with `cotal up`, then run `cotal down --preserve-state` again.\n\nAfter those checks, a retained bare apply stops the selected space's service instances before its\npidfile sweep. Each goes through the service controller with the checks and receipt of\n`cotal service stop <name> --instance <id> --yes`, and saved definitions stay. An instance whose\nprocess is already gone is skipped. Every instance is checked before the first stop, and one that\ncannot be verified refuses with no service stopped and the broker still running. An unreadable\nservice record keeps the code of its read refusal. A retained native attempt, an unsettled request,\na process that cannot be observed, or a record from another host, which bare `down` does not touch,\nrefuses `LIFECYCLE_BLOCKED`, exit 2. The remedy names that service and the command to run. A stop that does not confirm ends the sequence with the\nstack still running, and completed stops are not rolled back. `--with-agents` refuses when a\nservice would be stopped, because the legacy reap does not reach a service's agents.\n\n\n```bash\ncotal down\ncotal down --with-agents\ncotal down --abandon-stop <request-id> --yes\ncotal down --preserve-state [--store-dir <dir>] [--session-store <dir> \u2026]\ncotal down --dry-run [--space <name>] [--json|--plain]\ncotal down <service-name> [<service-name> \u2026] [--instance <id>] [--space <s>] [--yes|--dry-run] [--json|--plain]\ncotal down -f <cotal.yaml> | --run <id> [--dry-run]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--file <cotal.yaml>`, `-f` | none | Tear down this manifest's deploy |\n| `--run <id>` | none | Tear down one additive ledger, written by the retired `spawn -f` deploy, by its run id |\n| `--space <name>` | current space | With service names: the space whose saved definitions to stop. With bare `--dry-run`: confirm that space belongs to this root. A bare stop refuses it |\n| `--dry-run` | off | Print the manifest teardown, the named service stop or the bare stack preview, mutate nothing |\n| `--with-agents` | off | Bare whole stack only: also stop and deprovision every managed agent |\n| `--abandon-stop <request-id>` | none | Controller stack, with `--yes`: abandon a member's stop that no receipt settles, then stop the stack. See the settlement rules under [up](#up) |\n| `--preserve-state` | off | Bare whole stack only: fence the manager, retain principals and durable state, stop and prove the stack down, then publish `ready` |\n| `--store-dir <dir>` | `.cotal/nats` | With `--preserve-state`: the actual store path (required for a custom store) |\n| `--session-store <dir>` | none | With `--preserve-state`: a harness transcript store directory to capture with every continuation-capable retained seat. Repeatable. No default and never inferred from a connector name; a path that does not exist or is not a directory is refused before anything stops |\n\nBare `cotal down` stops the whole local stack in dependency order and leaves managed agents running\nwhen their runtime lets them outlive the manager. Before signalling the manager it verifies the spare\ncapability of the exact recorded manager, which records what that manager's stop does with its\nseats, and it reports the agents left behind plus `cotal down --with-agents` as the explicit reap.\nWhen the manager had no managed agents, it prints no report. When they cannot be listed, for\nexample on a user-auth mesh with no login on this machine, it prints\n`could not list managed agents (<reason>)` and the stop continues.\nThe built-in pty runtime keeps each PTY inside the manager process, so those seats cannot outlive\nit: every manager stop stops and deprovisions them, and `down` reports them as stopped. Every manager\nstop the CLI makes runs this one path: `down`, Ctrl-C on a foreground `cotal up`, the teardown after\nthat `up`'s broker exits, the leftover-manager stop before `cotal up -f`, and the delivery cutover.\nEach holds the manager's stop reservation, so a second stop while one is in flight is refused,\nnames the process holding it, and leaves that stop's `--with-agents` policy in place. On Windows,\nmanager and delivery stops send an authenticated local cleanup request because Windows process\nsignals terminate without running cleanup handlers. The request requires the process's private\ncapability, its creation identity and the live stopper's identity and reservation. Other platforms\nuse `SIGTERM`. A `cotal supervise` that receives `SIGTERM` or `SIGINT` while it is still starting\nlets the start settle, then runs the same stop and exits without printing `manager up`, so its\nliveness lease is released and an immediate restart is not refused. When that start fails, the stop\nstill releases the lease before the process prints the start error and exits 1. A process still running after 20s on the Windows cooperative path, or 15s after\n`SIGTERM`, receives `SIGKILL`. Ctrl-C stops the manager first; when that\nstop is refused or the manager's exit cannot be confirmed, Ctrl-C signals nothing else, prints the\nrefusal with the reap route, and leaves the stack running; end it with `cotal down --with-agents`.\n`--with-agents` is a one-shot destructive policy bound to the exact verified manager process\nand the exact live `down` stop reservation; a stale, malformed, crashed, or different stop attempt\ncannot turn a later bare shutdown destructive. If a managed agent cannot be proven stopped within\nthe manager's stop timeout, the manager logs which one, still closes its broker connections and\nconsole listener, and exits with code 1. It does not release its pidfile, liveness lease or\nservice registration in that case, so no successor is handed authority while that agent may still\nrun; the lease lapses on its TTL. A named `down` is the service stop above and never stops a\nlegacy component by its pidfile, so `cotal down manager` or `cotal down web` on a legacy stack is\nrefused unless a saved definition has that name. Only bare `cotal down` stops a legacy root's\ncomponents, all together, under the folder you run it in. It takes no `--space`. The\n`-f` / `--run` forms tear down a [manifest deploy](#manifest-deploys) without stopping the whole mesh\nand cannot be combined with service names or `--space`.\n\nA pinned manager with no spare-capability record is not signalled by bare `cotal down`. A current\nmanager always publishes the record, so a missing one means an older\nmanager: one that predates capability reporting, or one whose pty runtime reported that it cannot\ndetach its agents. Stop each managed agent explicitly, then run `cotal down --with-agents` from the\nmesh root to stop the whole stack. An older manager does not understand\nthe reap request, which is why the agents must already be stopped.\n\nAfter the known-blocker admission checks above, the retained bare `cotal down`\ninventories by pidfile. When this folder's registered broker answers and no\n`nats.pid` records it, the command does not say nothing is running. It names the space and the\nbroker address, says no pidfile records that process, says it will not stop a process it did not\nstart, and exits 1. Stop that broker with whatever started it (an init unit, a container, or the\nhand-run process). `cotal meshes rm <space>` only drops the registration. The probe runs whether or\nnot other owned components were running: they stop and clear their artifacts first, then the broker\nis named. A `--dry-run` does not run this probe.\n\n`down` reads each process record once. A component that exits and removes its own record while\n`down` runs counts as having no record, so the stop goes on. Any other failed read is an error.\nWhole-stack shutdown keeps delivery credentials until every required component has stopped.\nA refused stop preserves both the space-scoped credential and its legacy copy. Credential removal\nerrors remain failures. `down` removes the manager's delivery-aware marker, shutdown intent and\nspare capability only after every component has stopped, so a refused stop keeps them too.\n\n**Teardown verifies pinned process identity before signalling.** PIDs are recycled by every OS,\nso a recorded pid alone is not a durable target identity. `up` and the dashboard's native child record each\nprocess's creation identity in a sibling `<pidfile>.identity` pin, which holds the pid and the\nprocess start reported by the OS. Every stop path, including `down` for the broker, web and\nextension components, and the manager, delivery and auth-service stops, applies the same rule. A pin\nthat names a different start means the pid was reused, so teardown refuses and preserves it. A torn\nor unreadable pin also refuses. macOS start tokens keep their spaces, including the padded day,\nin both the identity pin and the one-shot shutdown intent. A single record cannot span lines.\nOn Windows, the reader calls `GetProcessTimes` through a bundled Node-API binding for x64 or\narm64. It requests limited process-query access and preserves the creation FILETIME as a decimal\nstring. It launches no subprocess. Access denial leaves identity unverified; an unreadable pin\nstill refuses teardown. A missing or damaged binding is an installation error and has no fallback.\n\nThe pidfile and its pin are published by renames, and the pidfile rename is the commit point. Just\nbefore it, the pin holds two lines: the old process's and the new one's. A launcher that dies\nmid-publish therefore leaves the old record or the new one, each checked against its own pin line,\nnever a pidfile without its pin. An old record with no pin is legacy, so its line holds `-` in place\nof the token and it stays legacy until the commit. A CLI older than this change reads a two-line pin\nas torn and refuses.\n\nPublishes of one pidfile are serialized by a lock file beside it, `<pidfile>.publish.lock`, because\nthe launcher and the daemon it starts both publish the same record. The next publisher reclaims a\nlock left by a crashed one. When no start token can be read for the new process, its pin line holds\n`-` in place of the token, which reads as a legacy record, and the publish ends in the legacy shape:\na pidfile with no pin. Teardown, and a daemon removing its own record on exit, take the same lock and\nremove the record only while the pidfile still names the pid they stopped, so a stop that races a\npublish leaves the new record whole.\n\nThe web dashboard service claims `web.pid` with an exclusive create, so a second dashboard for the\nsame mesh is refused, and writes its pin right after the claim. A stop that runs between the two\nreads a legacy record. Any existing `web.pid` refuses the start, whether or not its process still\nruns, and the start neither adopts nor reclaims it.\n\nThe pidfile pid and the pin pid are two coordinates. Automatic cleanup follows **proven death of\nthe pidfile target** (ESRCH on that pid): a torn sibling pin does not wedge a dead pidfile pid.\nA torn pairing where the pin names another pid, while the pidfile pid is still live or not proven\ndead, still refuses. Inspect both pids with `ps`. Do not delete `<pidfile>.identity` to force a\nstop; that weakens target-identity protection. Once the pidfile process is dead, rerunning\nteardown clears the stale record automatically.\n\nA Windows manager or delivery daemon without the cooperative capability must be stopped through\nits original supervisor before restarting with this version. Teardown refuses it without signalling\nor deleting its live record. It does not wait for a lease to expire or delete a lease by local pid.\n\nFor components using signals, the first teardown after upgrading a running pre-pin stack has a\nnarrower guarantee. A live record with no identity pin is signalled after a loud warning that it predates identity pinning. Restarting\nthe component writes the pin, so later teardowns receive full match and mismatch protection. The\nsame warning applies on platforms where no stable start token is available. For a legacy manager,\nbare `cotal down` also warns that agent sparing cannot be verified before it signals. Because the\nCLI cannot establish which SIGTERM handler that already-running binary carries, it never presents\nthe pre-signal seat inventory as confirmed spared; a genuinely older destructive handler may still\nreap those agents. `--with-agents` publishes a one-shot reduced-guarantee handoff bound to the\nrecorded manager pid and the live `.stopping` reservation's inode, then signals unconditionally.\nThat handoff cannot be replayed by a later stop attempt. A pin that exists and does not match the\nlive process still refuses before signal.\n\n`--with-agents` performs the old destructive logical teardown: managed processes stop and their\ncredentials, ACL rows, and delivery footprints are deprovisioned. `--preserve-state` is a different\nmaintenance transition: it stops retained processes while suppressing leave/deprovision cleanup, persists the manager's\nsame-principal resume inventory, stops the entire stack without removing run/auth artifacts, and\npublishes a stable inode-bound cut only after every recorded process is proven stopped and the exact\nrecorded NATS endpoint is unreachable. A missing or stale broker pidfile never counts as stopped. The\nattempt is bound durably before the manager is fenced, the resume document and attempt-bound\n`cut-intent` are fsynced before manager commit, and the manager's commitment itself is journaled\n(`cut-committed`) before any process stops. A retry after a crash at any of those boundaries reuses\nthe exact recorded attempt and finishes the remaining stop and endpoint proofs idempotently, without\nneeding the (by then intentionally dead) manager. A partial cut never publishes `ready`. It cannot\nbe combined with component names, manifest teardown, or `--dry-run`.\n\nOn a mesh that runs the delivery daemon, the cut stops it first so no join or leave can change\nmembership under the manager's inventory. Before any manager-owned endpoint stops, the cut then\nreads the presence roster and refuses while an endpoint the manager does not own is live. A refusal\nleaves the delivery daemon stopped. A deleted presence key is the only row it reads as absent. Any\nother row that is not a card with an `id` equal to the presence key it was read under and a string\n`name` refuses the cut with `presence record <subject> is malformed`. A participant can write any\nvalue under its own key, so the key is the identity the cut trusts.\n\n**Seat checkpoints.** After the stack is proven down, the cut writes one checkpoint per retained\nseat under `.cotal/maintenance/v1/checkpoints/<attempt>/<seat>/`, and prints the path, the\ncontinuity class and the generation for each. The path carries the preservation attempt because a\ncheckpoint is immutable once sealed: a shared directory would make the second cut in a root refuse\non the first cut's leftovers, and clearing it would destroy an artifact a rollback still needs. The\ncapture happens only at that point because anything earlier races a harness that is still writing\nits transcript and its working tree.\n\nEach checkpoint directory is created 0700, refuses a destination that already exists, and holds:\n\n- `repo.bundle`, the seat `cwd`'s reachable history, anchored on the base commit the record names\n by full object id;\n- `repo.index.diff` and `repo.worktree.diff`, the staging state as two diffs, base to index and\n index to worktree. Two rather than one because a single combined diff restores a mixed tree with\n the right bytes and the wrong index: a source reporting `MM README` would come back as ` M README`;\n- `repo.untracked.tar`, the untracked files in scope;\n- the harness session pointer, when the seat's connector declares one, and the transcript store\n files the operator named with `--session-store`. Each records where the destination puts it back\n as an anchor (the workspace root, the account home, or the seat's `cwd`) plus a relative path,\n because the destination's root and home are its own and the source host's absolute spelling would\n either miss them or write outside them;\n- `checkpoint.json`, written last, after every digest is computed over the bytes that landed.\n\nThe record carries the manager's resume entry unchanged as its first field, then the space, the seat\nname, the recovered `lifecycleUid`, the writer generation the cut was taken at, `capturedAt`, the\nrecency horizon, the applied profile revision, the seat's `git status --porcelain` as the cut read\nit, and the continuity class. Every captured file is\nlisted with its byte size and sha256, so an operator verifies the whole artifact with `sha256sum`\nand `git bundle verify`. No secret values, no operator keys and no source-host launch material\nenter it.\n\nThe generation is the highest one this root has claimed for the seat in `.cotal/auth`. A\ngeneration entry there that is a symlink, a directory or anything else that is not a regular file\nrefuses the cut, as every other auth record does.\n\nThe continuity class is what the connector declares, capped by what the checkpoint carries. A\nconnector declaring session continuation classifies as `exact`, but reopening a session takes both\nhalves, the pointer that names it and the store that holds its transcript. A checkpoint missing\neither one cannot reopen that session, so it is recorded as `fresh` when the connector declares a\nfresh start and `drain-only` otherwise. A pointer with no store is capped the same way as a cut\ncarrying neither, because it names a session whose bytes the artifact does not contain. A class is a promise the destination is entitled\nto act on, so it never describes bytes the artifact does not contain. The transcript store stays an\noperator input: this repository does not know where a harness keeps its transcript, so `exact`\nrequires `--session-store` to name one.\n\nThe recorded status is read under the same selection rule as the untracked set, so it describes the\nstate the captured bytes can reproduce. The destination re-reads it in the promoted tree and refuses\na difference.\n\nThe untracked selection rule is recorded in the record and is\n`git ls-files --others --exclude-standard -z, excluding .cotal/`. It honors `.gitignore`, so an\nignored file the seat needs does not travel and has to be moved separately. The `.cotal/` exclusion\nis a secrecy boundary rather than a size one: when a seat's `cwd` is also the mesh root, the control\ndirectory is untracked, and without the exclusion the broker trust material, the space account, the\nmanager instance identity's private seed and the seat's own credentials would land inside the\nartifact. A checkpoint carries credential references only; the destination resolves that material\nitself.\n\nA seat whose launch options could not be resolved is refused rather than checkpointed, with the\nmanager's own wording: `imperative launch options have no non-secret durable source (<keys>)`. The\nrefusal arrives at prepare time, so the cut stops before any child does.\n\nA delegated seat (SPEC \xA713.17) is refused at prepare time too, with\n`a delegated seat is not resumed by a later manager; stop it before preserving`. A manager stop\nafter a refused cut retires that seat through its retirement path.\n\n## clean\n\n```bash\ncotal clean <history|store|all> --force\ncotal clean restore-attempt --attempt <id> --force\ncotal clean restore-fallback --attempt <id> --force\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | `history`: target mesh |\n| `--dms` | off | `history`: also clear DM history |\n| `--store-dir <dir>` | `.cotal/nats` | `store`/`all`: JetStream store directory |\n| `--force` | none | Required: destructive, no prompting |\n| `--attempt <id>` | none | `restore-attempt`: exact stale pre-commit attempt; `restore-fallback`: matching healthy committed restore |\n\nOne configurable cleanup verb; every target requires `--force`.\n\n- `history` purges the retained message backlog on the **running** broker (channels, plus DMs\n with `--dms`). The same operation as [`history clear`](#history), which stays as an alias.\n- `store` deletes the **stopped** mesh's JetStream store (`.cotal/nats`): streams, durable\n consumers, and messages. This is the reset for stale on-disk broker state, e.g. durables\n minted by an older, incompatible Cotal generation surviving a `down`/`up` cycle.\n- `all` is `store` plus the space identity (`.cotal/auth`), the local creds and markers tied to\n it, any crash residue a normal `down` would have swept (stale pidfiles, `run/`), and the mesh's\n registry entry; the next `cotal up` mints a fresh identity.\n\n`history` needs the mesh up; `store` and `all` refuse while any recorded mesh process may still be\nrunning or any same-root recorded broker endpoint remains reachable (run `cotal down` first). A\nrecord counts as stopped only when it is absent, empty, or names a pid the kernel reports gone, so\nunattributable content, a removal reservation, or a pid whose liveness cannot be read refuses. They\nalso refuse outright on a root that holds accounts for several spaces: the store and the broker\ntrust record are shared by every space on the broker, so both targets would take out all of them\nand no `--space` can narrow that. `down`, `backup` and `up --restore` refuse there for the same\nreason. `cotal status` lists the tenants on such a root. Personas\n(`.cotal/agents`) and logs are never touched. The mesh record now carries a custom\nstore location for `up`'s own repair, but `clean` still takes `--store-dir` itself; `clean` does\nnot read the record. Custom cleanup targets must contain either the Cotal store-generation marker or a\nreal `jetstream/` store directory; filesystem roots, project roots, and Cotal auth/maintenance trees\nare always refused.\n\n`store` and `all` also refuse every maintenance journal state. After a healthy committed restore,\n`restore-fallback` is the only supported way to remove the recorded unchanged old-store inode; it\nnever deletes the active target, requires both the exact attempt id and `--force`, and retires the\ncompleted restore journal so a later `down --preserve-state` can start a new backup cycle.\n\n## reset\n\n```bash\ncotal reset store <space> [--dry-run] [-y|--yes] [--json|--plain]\ncotal reset space <space> [--dry-run] [-y|--yes] [--json|--plain]\n```\n\nBoth routes select a space this machine started. A space with a locally started registration is\nselected by it. A controller stack that a completed `down` stopped has no registration; its\nprepared record and stack journal at the same root name its broker address, store and mode, and a\nreset keeps both, so a later `up` starts it again. A stack whose last operation is not a completed\n`down` refuses `LIFECYCLE_BLOCKED` and names `cotal down --yes`. Each refusal names one reason and\nthe command that clears it. A space that may still run names `cotal down` from its root, or\n`cotal up` and then `cotal down --yes` when a member exited without a recorded stop.\n\nThe store dry-run is a local-impact preview that exits 0. It requires a space selected that way, a\nreal project `.cotal` and a root readable as one local tenant. It opens with a verdict: the apply is\noffered, with `cotal reset store <space> --yes` as Next, or it would refuse, with the apply's own\nreason and remedy. The JSON result carries it as `applyVerdict`, and `completeness` stays\n`partial`. Completion suggests locally started names, without proving\neligibility. The preview inspects the store path with the existing guarded\ncleanup path checker, reads the maintenance journal, legacy process records,\nservice-controller instances, supervisor receipts, retained native attempts\nand root-local additive ledgers and attempts plus fresh boot attempts, and\nmakes a credless broker probe. Readable deployment IDs, unsettled attempts and unreadable\nrecord paths remain visible; these records do not\nprove live ownership or a complete broker tenant inventory. An orphan receipt\nor attempt remains visible without an instance record; these space-scoped\nrecords are not proven owned by this root. The preview also lists other local\nregistrations at the exact\nrecorded broker address. A match blocks store apply, while no match does not\nexclude aliases, remote tenants or live attachments. A negative credless probe\ncannot prove absence for TLS-first listeners. A custom store path must be\nabsolute. The preview takes no maintenance lock and cannot establish exclusive\ncustody or a complete identity deletion set.\n\nBare `reset store <space>` checks those conditions, displays a scoped terminal\nconfirmation or requires `--yes` in a script, and acquires the maintenance lock.\nUnder the lock it rechecks the exact registration or stopped-stack records, one-tenant root, absent\nmaintenance journal, legacy process records, service instances, supervisor\nreceipts and retained native attempts for that space, additive ledgers and\nattempts plus unsettled fresh boot attempts under the root, credless broker\nnonresponse and pinned filesystem identity before calling the existing\nstore-only cleanup. A readable or unreadable additive ledger or attempt\nblocks reset before confirmation and under the lock. It refuses another\nlocal registration at the exact broker address before confirmation and\nrechecks under the lock. This does not prove\nexclusivity against aliases or remote tenants. An orphan receipt or attempt\nblocks reset even when no instance record remains; a submitted native step may\nhave changed state. A TLS-required or TLS-first target refuses\nbecause a credless probe cannot prove it is down. The route preserves trust,\ncredentials and registration. A cleanup with an unconfirmed outcome is reported\nas unknown and is not retried. Credless nonresponse and the recorded process\nchecks are not proof that no unrecorded process exists.\n\n`reset space <space>` adds local identity cleanup through the existing native\nclean-all operation. This route currently requires Linux, a space selected as above and a\nplain-NATS loopback listener. It removes a locally started registration; a stopped controller stack\nkeeps its prepared record and stack journal. TLS-first, remote, shared-root\nand non-Linux targets refuse. It applies the same maintenance, deployment and\nprocess-custody guards as store reset. It does not stop a process to become\neligible.\n\nIts `--dry-run` validates the existing local trust chain and inventories the\nnative identity paths, including provider-declared secret keys, without running\ndeprovisioning hooks. The inventory is bounded to 10,000 paths. Foreign space\nsegments, symlinks, hard-linked files, foreign ownership, group/world-writable\npaths and providers without bounded cleanup metadata refuse. The refusal names the path and its\nreason; for a group- or world-writable path it names `chmod go-w <path>`. Values and content\nfingerprints are not printed. No maintenance lock is taken by the preview. A preview that passes\nevery check says the apply is offered and names `cotal reset space <space> --yes` as Next.\n\nApply shows the path inventory and defaults to cancellation, or requires\n`--yes` in a script. Under the maintenance lock it repeats the eligibility\nchecks and compares the reviewed file identities. Native secret deletion is\nrestricted to the planned keys; each filesystem removal checks the remaining\nreviewed tree. Added paths and changed files do not enlarge the confirmed scope.\nThe native operation deprovisions secrets before raw identity removal and\nremoves trust last. After checking the planned targets absent, it removes only\nthe unchanged eligible local registration and releases its matching selection.\nA replacement registration is kept and reported as partial.\n\nThe receipt retains acknowledged deletion paths even if later cleanup fails, and lists each path\nonce.\nUnconfirmed effects are unknown and are not retried. Personas, saved service\ndefinitions and stopped-instance records, installed packages, login sessions\nand remote data remain. Local cleanup does not revoke a remote credential or\nclear Cloud data.\n\n## Backups\n\n```bash\ncotal down --preserve-state [--store-dir <dir>]\ncotal backup create <dir> [--only full|registry] [--store-dir <dir>] [--json|--plain]\ncotal backup inspect <dir> [--json|--plain]\ncotal backup verify <dir> [--json|--plain]\ncotal backup recover <full-attempt-id> [--yes|--dry-run] [--json|--plain]\ncotal backup cleanup-fallback <full-attempt-id> [--yes|--dry-run] [--json|--plain]\ncotal backup restore <dir> [--only registry] [--store-dir <dir>] [--accept-missing-source] [--json|--plain]\ncotal up --restore <dir> [--restore-only registry] [--accept-missing-source]\n```\n\nOn a controller root, one with a stack journal, `backup create`, `backup restore` and\n`up --restore` refuse `UNSUPPORTED` (exit 2) in this release. Each needs a preserved cut, and\n`down --preserve-state` cannot make one there. They refuse while validating their arguments,\nbefore the connector seed or any write. Plain `down` and `up` keep a controller stack's store,\nchannels and history. Backup and restore of a controller-owned stack return in a later release. The\nrest of this section applies to a root without a stack journal.\n\nBackup is offline-only. It requires the stable `ready` record from `down --preserve-state`, an exact\nstore match, no recorded process that may still be running (the same test `clean` uses), and an\nunreachable exact endpoint from the recorded cut. That endpoint is probed immediately before cloning, so a live broker with a missing or stale pidfile\nis still refused. It claims the cut, reflink/copies the stopped source to a\nprivate attempt clone, and opens only that clone on a random loopback bootstrap broker with an\nindependent parent/deadline watchdog. A bootstrap broker that does not accept connections within\n15 seconds fails the command. Creation returns one JSON/plain receipt after cleanup.\nA published artifact plus incomplete cleanup is partial and retains the artifact and original\nattempt; it is not a reason to repeat creation blindly. It validates the canonical stream and pull-consumer inventory,\nwrites native snapshots with consumers excluded, and stores conservative contiguous ACK-floor\ncheckpoints separately. The presence bucket is memory-backed, so it does not survive the cut and\nthe clone may lack it. Every other stream must be present. The original store is never opened by\nthe backup broker, and the stack is not restarted implicitly. Artifact destinations must not overlap\nthe preserved source or maintenance\nattempt tree. Restore artifacts and targets likewise cannot nest inside or contain each other, the\npreserved source, or the maintenance attempt tree.\n\n`backup create` checks its preconditions before it claims the cut or creates any maintenance state.\nA root with no preserved cut refuses and names `cotal down --preserve-state`. A cut in another state\nrefuses and names that state and its space. A destination that already exists is a `CONFLICT`\nrefusal, because create writes only a new directory.\n\n`backup inspect` reads the bounded manifest and exact declared file-name inventory. It does not\nhash snapshot or checkpoint contents. `backup verify` additionally reads each named regular file\nand compares its size and SHA-256. Both are read-only: no staging directory, broker, restore or\nsecret upload. Reports omit snapshot bytes, configuration bodies and trust material. JSON retains\nthe declared metadata; terminal file lists are bounded and state their omissions. When a declared\nfile is missing, an undeclared file is present, or a file does not match its declared size and\nSHA-256, the refusal names that file and the fault, and its JSON target carries `file` and `fault`.\n\nFile integrity is not a signature or a restore proof. Matching a manifest does not authenticate\nits author, verify snapshot semantics or prove existing trust will accept a restore. The guarded\nread path refuses unsupported file-protection environments rather than using a weaker reader.\n\n`backup recover <full-attempt-id>` uses the original root's pre-commit restore journal, not a\nmanifest path or selected-space guess. It requires the native deadline and every recorded owner\nto prove the attempt stale, then performs the native rollback. Commit intent forbids rollback.\n`backup cleanup-fallback <full-attempt-id>` requires the matching healthy active restore and\nremoves only its unchanged retained old-store identity; it never removes the restored target.\nBoth review full paths and default to Cancel. Scripts need `--yes`; `--dry-run` reads the plan\nwithout taking a maintenance lock or writing. Apply rechecks the reviewed journal revision under\nthe native lock, preserves unknown outcomes and never retries a deletion. No live-owner override\nor process kill is supplied.\nThe full attempt id has the form `restore-<uuid>`. `cotal status` shows the attempt a root holds\nunder Maintenance, with its state and next step. A refusal for the wrong state names the next step\nfor the recorded one. A completed `cleanup-fallback` retires the journal, so a repeated cleanup\nrefuses with `TARGET_NOT_FOUND` and says the root holds no restore attempt.\n\n`backup restore <dir>` is the canonical restore. It runs the same guarded restore as\n`up --restore <dir>`, with the same lock, claim, trust and space proofs and the same resume, and\nreturns one `backup restore` receipt: the restore attempt, space, root, selection, store, broker\nprocess and whether the manager, delivery and auth service started. The restore follows the\nartifact's own selection, and `--only registry` narrows a full artifact to the registry. Any other\n`--only` value is refused, because the restore could not honor it. The space, broker and runtime\ncome from the preserved cut, so no selector can rebind them. A receipt means the active journal was\nwritten. Its `seatResume` field says whether retained seats were handed to the manager's resume\n(`requested`) or dropped by a registry restore (`not-requested`). It does not confirm that any seat\njoined. Restore needs the stopped preserved cut from `down --preserve-state`. A root with no\nmaintenance journal refuses before the restore takes its maintenance lock, so the folder keeps no\nstate from the attempt, and `up --restore` refuses the same way. A restore that fails after it\nbegan names its attempt in the text and in the JSON target, which also carries `journalState`, and\nits remedy gives the next step for that state. `up --restore`\nremains as the equivalent startup form, and its terminal output is unchanged. Unlike the other\nbackup modes, `backup restore` runs the connector seed first, as `up` does, because it resumes seats.\n\nTwo commands refuse an active restore. `down --preserve-state` begins a new cut only after\nthe restore journal is retired, so it names `backup cleanup-fallback` as the next step. `team boot`\nresumes only a stopped stack whose manager never launched, and a restored stack's manager already\nran, so start it with bare `cotal up` instead.\n\nStopped client-managed KV ordered consumers are ephemeral read residue, not backup state. Backup\nignores only the pinned client's exact stopped shapes: ordinary last-value watchers and the\nwhole-bucket scanner that uses all-history delivery to collapse concurrent tombstones. A bound\nconsumer or any lookalike with a different filter, inbox, lifetime, or other config is still refused.\n\n`full` is the default and indivisible: channel registry, CHAT/DM/TASK/INBOX/DLV, ACL, MEMBERS, and\nvalidated durable checkpoints. `registry` is the sole partial artifact. Presence, derived membership\nfeed, leases, native ephemeral/history consumers, credentials, keys, tokens, owner secrets, and actor\nledger files are excluded. `full` means every transferable message and registry stream, not every\nJetStream resource: endpoint submissions/facts/events/timers/workflow state, contract artifacts, and\nthe records/auth/session stores are nonportable control state. Restore recreates those streams empty\nwith their canonical configs before exposing the normal listener, so active endpoint runs,\nlifecycles, and sessions do not cross a backup. Artifacts are exclusively created `0700`;\nsnapshot/checkpoint files and\nthe manifest are `0600`; `manifest.json` is written last with exact sizes and SHA-256 values. The\ndirectory is trusted operator input: hashes detect corruption, not malicious rewriting.\n\nRestore validates and stages the exact allowlisted artifact bytes before moving or creating a store.\nIt requires the same space and existing trust state. The whole pre-commit window holds a journaled\nliveness claim (coordinator, watchdogs, brokers, absolute deadline): ordinary `up` and a repeated\n`up --restore` refuse while the claim is live, and a stale attempt is recovered only after the\ndeadline has elapsed and every recorded owner is proven dead. A retried `up --restore` handles this\nautomatically; an operator can also recover it explicitly with `cotal backup recover <full-attempt-id> --yes`. Nothing\never rolls back a live attempt. A registry-only artifact restores as registry-only whether or not\n`--restore-only registry` is passed; omitted infrastructure is always created and the exact\npost-restore stream inventory is asserted before commit intent. Ordinary `up` from a preserved cut\nresumes only the exact recorded source store and runtime; a contradicting `--store-dir` or\n`--runtime` fails in preflight.\n\n**Admitting a seat checkpoint.** An ordinary `up` from a preserved cut admits that cut's seat\ncheckpoints before it journals the resume attempt and before any process starts, so a refusal costs\nnothing. Three gates run in order, each naming what it saw.\n\n1. *Integrity.* Every file the record names must be present, a regular non-symlink file, the\n recorded byte size and the recorded sha256, re-stat'd after the read so a file that moved is a\n refusal. Failure here consults no other gate.\n2. *Identity.* The recorded space must match, the recorded `lifecycleUid` must not belong to a live\n incarnation, and the profile revision must match this host's or be resumed under deliberately\n this host's. A differing revision is refused with both digests and the remedy, and there is no\n override: the checkpoint carries the recorded digest and not the config bytes, so nothing could\n run the seat under the recorded revision, and the manager re-digests the same file and refuses\n drift on its own. This gate has no blanket override, which is the only reason the next one may\n have one.\n3. *Recency.* `capturedAt` is compared to this host's clock against the horizon the record carries.\n Inside it, the seat resumes. Outside it, `up` refuses and prints the capture instant, the clock\n reading and the horizon; `--accept-stale-checkpoint` admits it anyway and the exercised consent\n is printed with the actual age. An unreadable `capturedAt` is refused with no override, because a\n freshness gate that fails open is not a gate.\n\nCustody transfers only after all three pass. The destination claims the recorded generation plus one\nby exclusive create, before it launches anything. A lost create means another destination is already\nclaiming that seat, and it refuses with `seat-writer-generation-create-lost` rather than adopting\nthe winner and becoming a second writer. The recorded `lifecycleUid` is reused and never minted, so\nthe resumed seat binds the same lifecycle-keyed durables.\n\nAdmission is reconciled against the inventory the resume is about to hand the manager, and that\nreconciliation finishes before the restore moves a single tree. A checkpoint whose recorded\n`lifecycleUid` is not the one the retained inventory carries describes a different incarnation of\nthat seat, and it refuses with both uids while every live working tree is still untouched and no\ngeneration is claimed. A retained\nseat with no admitted checkpoint refuses the resume by name: an absent checkpoint directory and an\nabsent record are indistinguishable from a seat that was never checkpointed, and a seat that starts\nwithout passing the gates has claimed no generation. `--accept-stale-checkpoint` is recorded in the\nresume journal with the seat, the capture instant, the admitted age and the horizon, so the consent\nsurvives the terminal it was typed into.\n\nThe whole admission is all or nothing. Coverage is settled first, then every gate runs over every\ncheckpoint, and only then is any generation claimed. A refusal at any point leaves every generation\nunclaimed, including a lost exclusive create during the claim itself: the claims that attempt made\nare removed before the refusal is raised, by the exact paths it wrote, so a generation another\ndestination holds is never touched. A claim is a create that can never be made again, so a refusal\nthat left one behind would consume the retry over the same checkpoint set.\n\n**Restoring a seat checkpoint.** Once every gate has passed over every checkpoint, and before a\nsingle generation is claimed, `up` puts each admitted seat's captured bytes back. A refusal here\ncosts nothing for the same reason a gate failure does: no claim has been made and nothing has\nstarted.\n\nA restore never moves or replaces the destination's own control directory. A checkpoint excludes\n`.cotal/` by design, so a seat whose `cwd` holds one, which is the layout an operator gets by\nrunning `up` and `spawn` in a single directory, is refused before anything is staged: promoting a\ntree that cannot contain `.cotal/` over that `cwd` would carry this host's live trust material and\nmaintenance state away with the superseded tree. The refusal names the control directory it found\nand the remedy, which is to give the seat a working tree that is not a workspace root.\n\nEach seat is staged beside its own `cwd`, in `<cwd>.incoming`:\n\n1. every recorded digest is verified again over the files as they are now;\n2. the bundle is cloned into `<cwd>.incoming`, which is refused when that path already exists;\n3. the recorded base commit is verified in the clone and checked out detached, so a bundle that does\n not contain it stops the resume instead of continuing against a different history;\n4. the index diff is applied with `--index` and the worktree diff without it, both `--binary\n --allow-empty`. That order is what puts staged content back in the index rather than only in the\n worktree, and `--allow-empty` is why a seat with a clean tree is still restorable;\n5. the untracked archive is extracted.\n\nEvery seat stages before any seat is promoted. Promotion moves an existing `cwd` aside to\n`<cwd>.superseded.<timestamp>` and renames the staging directory into place, then puts the session\npointer and store files where the destination's connector reads them, then re-reads\n`git status --porcelain` in the promoted tree and compares it to the status the checkpoint recorded.\nA restore that applied without error and produced a different index is a refusal, not a warning. The\ntwo renames are the only steps that touch the path the seat will use, so a failure anywhere leaves\nevery seat's live `cwd` as it was.\n\nThe rename itself claims the superseded name, and a taken name gets a numeric suffix. The timestamp\nhas one-second resolution, so two promotions of the same seat within one second compute the same\npath; a rename onto a name that already holds a tree fails on every platform, and that failure is\nread as taken. Nothing creates the name ahead of the move, because Windows refuses to rename onto an\nexisting directory at all. A superseded tree is the thing that rename exists to keep.\n\n`git` and `tar` run as child processes with argument arrays, never a shell string.\n\nA leftover `<cwd>.incoming` refuses the resume by name. A staging directory from a failed run is the\nonly record of what failed, so nothing removes one automatically: inspect it, remove it by hand, and\nresume. A pre-existing `cwd` is renamed rather than deleted, so a wrong checkpoint costs a rename\ninstead of a tree. When a promotion fails, the renames that attempt made are undone and the staging\ntree is left where it is, as the evidence for what did not verify.\n\nA session pointer whose recorded `sessionId` is not the one the retained inventory reopens is\nrefused before anything is cloned. A session file already present at its destination is judged by\ncontent: bytes equal to the recorded digest are already restored, and different bytes under the path\nthe connector is about to read are refused with both digests rather than clobbered.\n\n`up --restore <dir>` reaches the same admission and the same restore, after the store is restored\nand validated and before commit intent is journaled. A registry-only restore resumes no seat, so it\nadmits and restores nothing.\n\nOne limit is worth stating plainly. The writer generation is claimed by exclusive create inside one\nworkspace root, so it fences two resumes on the same host and does not fence two independent\ndestinations: copy a checkpoint to two roots and both claim the same successor. A real cross-host\nfence needs a coordinate neither root owns.\n\nAuthenticated restores validate the complete\nspace trust bundle before staging, including nkeys, seed matches, JWTs, signers, and space binding;\nfull restores commit to the validated operator, system-account, data-account, and active-signer root\nchain in addition to the static/user authority fingerprint. Because the system account is part of that\ncommitment, a [`cotal up --rotate-sys`](#up) makes every full artifact taken before it unrestorable\nagainst this root: take a fresh full backup after each rotation. The composed commitment is revalidated\nimmediately before store mutation and never includes secret seeds. Restore never creates fresh auth.\nSame-path restores atomically retain the old\nsource at the journaled fallback path; alternate targets retain it in place; a missing canonical\nsource needs explicit `--accept-missing-source`. Quarantine and target restores use current canonical\nconfigs on isolated random-loopback brokers, never expose native snapshot consumers, and publish a\ncommit-intent immediately before the normal listener starts. Archive bytes never instantiate the real\ntarget: after quarantine validation, every stream is re-snapshotted from the validated quarantine\nstate into attempt-owned sanitized files, and the target is restored solely from those. Before that boundary, failure rolls back\nthe attempt-owned target; after it, ambiguity preserves both stores and records forward-repair\nrecourse. The cooperative maintenance lock excludes Cotal commands, not arbitrary raw NATS processes.\n\nBootstrap brokers in every auth mode, including open, mount the store under a local account with\nrandom operation-specific logins only, each carrying the exact per-phase subject permission matrix;\nnormal static credentials and user-auth sentinel/bearer connections are rejected, and no auth\nservice or callout starts. Open mode differs only in its account label, never in authority. Inventory, each stream snapshot,\nrestore initiation, exact upload id, validation, and each checkpoint recreation use separate exact\nauthorities. Every checkpoint carries the source stream's message/first/last sequence state and must\nmatch its snapshot record before mutation; core then derives and validates the only allowed start\npolicy. TASK is not a CLI exception: the same core checkpoint API recreates its canonical `DeliverAll`\nWorkQueue durable because acknowledged tasks are absent from retention and NATS forbids a\nstart-sequence policy there. Registry-only restore creates every omitted canonical stream and transient\nbucket on the isolated target before the normal listener is exposed. It deliberately does not resume\nretained agents or recreate their DM/DLV/TASK/ACL state; their identity material stays retained and\nstopped rather than being reprovisioned into a partial restore.\n\nAfter listener readiness, the manager starts attempt-bound, validates retained credentials/tokens\nwithout granting or reprovisioning, and resumes the exact persisted principals under cleanup\nsuppression. Registry-only restore uses the same flow with an empty agent set. On a user-auth mesh\nthese manager calls run as the logged-in operator's `cli` actor, the caller the preserve cut used, so\nthat actor needs a current `admin` grant. `commitResume` is an\nidempotent validation barrier only: success must be `awaitingFinalize` with an attempt-bound 64-hex\ncommit token and does not release suppression. Under the workspace lock, the CLI first fsyncs that\nexact evidence as `manager-committed` (restore) or `resume-committed` (ordinary resume), then calls\ntoken-bound `finalizeResume`; only an `active` response for the exact token releases suppression. The\nCLI records the same token in finalization evidence before a restore becomes `active`, or before an\nordinary resume retires and consumes the marker. A new launch hands the token to the next manager as\na protected file: the CLI journals its path, writes it at mode 0600 in\n`.cotal/maintenance/v1/handoff/`, then journals its inode and digest before the child starts. The\nchild reads it once with `--resume-commit-file` and `--resume-commit-sha256` and removes it. The\ntoken never appears on a new launch's command line. Before retirement or activation the CLI settles\nthat file. A file it did not record, or a recorded path that now holds another file, holds the\nattempt and is never deleted. `cotal supervise --resume-commit-token` stays accepted for older\ncallers; while such a process runs, its token is visible in its command line. Re-entry from either committed state skips the prior\nidempotent activation/commit phases, retries finalization with the durable token, and finishes the\nworkspace transition. Failure before finalization preserves the committed state and cleanup\nsuppression; it is not rewritten through a degraded transition. Re-entry between any two earlier\nboundaries reuses the same attempt and may retry the idempotent phases without deleting retained state. A missing or\nchanged per-agent dependency is a named fail-closed result; the journal becomes degraded and remains\navailable for forward repair. A retry from `resume-intent`,\n`resume-active`, or `resume-degraded` reuses the same attempt and inventory after the prior listener is\nproven stopped. A retained agent the lost manager already launched can still be running, for example\nin a tmux window, while the journal reads `resume-intent`. On a static mesh the replacement manager\ncloses that seat through the reference the lost manager recorded on the agent's slot, waits for the\nprincipal to leave presence, and launches it again. A live principal with no such record, or one that\nstays live after the seat is closed, is refused. Every normal restore listener has an unguessable\nattempt-bound NATS server name. The CLI fsyncs its exact name/nonce, canonical endpoint, process owner, and generation-bound target identity\nimmediately after spawn. Re-entry accepts a surviving listener only when its INFO server name, live PID\nrecord, endpoint, and target identity all match that proof; degraded restore repair then moves through\nthe guarded workspace transition only after manager commit. If an uncommitted bound owner is provably\ndead, recovery retires that exact proof under the maintenance lock and binds a fresh listener for the\nsame attempt, endpoint, and target with a new nonce and server name. A live foreign/mismatched listener\nor ambiguous owner is preserved and refused, never adopted by reachability alone. A reconstructed\ncommit/degraded attempt without either the exact bound proof or a durable dead-listener replacement\nrecord fails closed even when the recorded port is free. A later ordinary startup may pass an `active`\nrestore only when its details prove manager commit and its exact recorded listener is dead.\n\n## Selecting a space\n\n```bash\ncotal space [--json|--plain]\ncotal space show [<name>] [--json|--plain]\ncotal space add <name> --server <url> [--root <dir>] [--mode auth|open] [--tls] [--allow-unencrypted-overlay] [--force] [-y|--yes]\ncotal space add [<name>] (-f|--file <bundle.json> | --from <https url>) [--root <dir>] [--server <url>] [--force] [-y|--yes]\ncotal space create <name> [--root <dir>]\ncotal space forget <name>\ncotal space delete <name> [--dry-run] [-y|--yes] [--json|--plain]\ncotal use [<space>] [--json|--plain]\n```\n\n`space` lists the spaces this machine can reach: each one's broker state, server, mode, origin\n(`started here`, `registered` or `discovered`) and root, with the selected one marked. It probes\neach broker that this machine started or that was registered by hand, and reports one that does not\nanswer as `unreachable`; its registration stays. A discovered space is never dialed by a listing and\nreads `not checked`. `space show [<name>]` shows one space in full, the selected one when no name is\ngiven, and probes only that broker. Neither writes anything.\n\n`--json` writes one document. `space` gives `current` (the selected space or null),\n`currentRecorded`, `items` and `prepared`; each item is the row `cotal status --json` gives under\n`meshes.items`: `space`, `server`, `mode`, `root`, `origin`, `tlsRequired`, `eventsRequired`,\n`current`, `check` (`reachable`, `unreachable` or `not-checked`) and `catalog`. `space show` adds\n`recordedAt`, `attachHost`, `maxSessions`, `storeDir`, `unencryptedOverlay`, the login pins of a\nuser-auth space under `userAuth` and the catalog's `id`, `slug`, `kind` and `role`. It leaves\ncredential paths out. A name that is not registered is refused with `TARGET_NOT_FOUND`, exit 2, and\nso is a bare `space show` with nothing selected. Each entry under `prepared` has `space`, `root`,\n`preparedAt` and `current`, and `space show` of a prepared space gives those with the kind\n`prepared-space`.\n\n`space add` registers a space that runs elsewhere, so `--space` and `use` can reach it. With\n`--server`, the trust comes from `--root`'s `.cotal/auth`, and the mode is `auth` when that folder\nholds the space's trust, `open` otherwise. With `--file`, a user-auth connection bundle exported\nwhere the space runs supplies the login pins and the server, unless `--server` names another\naddress for that broker; `--from` fetches the space's discovery\ndocument instead, after you agree to the fetch, and needs a terminal. Before anything is written,\n`space add` checks that the address may carry credentials, that the trust composes, that the\nbroker enforces the mode, and that the credentials open the space; a user-auth space's exchange\nmust answer as the issuer it pins. On a terminal it then shows what it will trust and asks; `--yes`\nskips that question, but not the confirmation of fetched pins. It never changes the selection.\n\nA refusal writes nothing and carries its code: `USAGE_INVALID` or `SCHEMA_INVALID` (exit 1) for\ninput, and `CONFLICT`, `TARGET_UNAVAILABLE`, `DEPENDENCY_UNAVAILABLE`, `UNSUPPORTED`,\n`AUTH_REQUIRED` or `PERMISSION_DENIED` (exit 2) for what the checks found. A name that is already\nregistered is a `CONFLICT`. `--force` replaces a registration added by hand, never one this machine\nstarted or a catalog lists, and adds a `--server` space without checking its broker, which suits a\nspace that is down right now; a user-auth space is always checked. Declining the question is a\n`no-op` with a `USER_CANCELLED` warning. `--json` gives the registration under `space`, in the\nshape `space show` gives, with `verified` and `replaced`.\n\n`space create <name>` prepares a space to start on this machine. It records the name and the root,\nwhich is `--root` or this folder, and makes `<root>/.cotal/`; `cotal up` in that root then starts the\nspace under that name and drops the prepared record. It also writes the `default` persona into\n`<root>/.cotal/agents` when the root has none, as setup does, and leaves one you edited as it is.\n`--json` lists the files it wrote under `result.written`. The project and its ancestors must be physical\ndirectories owned by you or root, with no symlinks. World write refuses. Group write is allowed only\non a directory you own whose group is your user private group, the Debian and Ubuntu default, and\nthat has no extended ACL. On Linux, `getent` must show that the group is your primary group, lists\nno other member and is no other account's primary group, and `getfacl` must show no ACL entries\nbeyond the base ones. That account list counts only when `/etc/nsswitch.conf` names `files` and\n`systemd` alone for passwd and group. A directory source such as LDAP or SSSD can list part of its\naccounts, so with one configured, group write refuses. `getent` and `getfacl` run from your PATH\nwith a 2 second limit each, so the first check in a process can take a few seconds. A proven group\nholds for the life of the process. When a read is unavailable, group write refuses; install the\n`acl` package for `getfacl`, or remove group write. Root-owned sticky temporary directories are\nallowed. New control directories use mode 0700. An unsafe existing path refuses before the\nprepared record is written, without changing its permissions. The refusal names the path, the\nreason and a remedy such as `chmod g-w <path>`. It starts nothing and leaves the\nselection as it is. It refuses with `CONFLICT` a name that is registered or prepared, a folder that is already a\nspace's root, and the home folder, whose `.cotal` holds this machine's own records. `space` lists\nprepared spaces under their own heading, and `space show` and `use` accept one. A command that\ntargets a prepared space refuses with `TARGET_UNAVAILABLE`, exit 2, naming the root to start it in,\nand so does `cotal up --space <name>` run from another folder. `cotal up --space <name>` in a folder\nthat is already another space's root refuses with `CONFLICT`, exit 2. `space add` refuses a prepared\nname.\n\n`space forget <name>` removes a registration this machine keeps. It stops nothing, deletes nothing on\nthe broker, and keeps the files under its root. It refuses a space this machine is running with\n`LIFECYCLE_BLOCKED`, because `cotal down` stops it and drops the record, and a discovered space with\n`CONFLICT`, because its catalog keeps it. A space whose root holds its stack journal counts as\nrunning while any stack member's process may still run. The refusal names one such member and\nwrites nothing: the registration, the selection and the root's files stay. A member whose process\nis gone without its stop receipt does not block, and the result warns with `STACK_MEMBER_UNSETTLED`\nthat it was never stopped cleanly. A prepared space is forgotten the same way, and the `.cotal/`\nthat `space create` made stays. Forgetting the selected space clears the selection.\n`status` in a forgotten space's root then selects no space and probes no broker.\n\n`space delete <name> --dry-run` reads the registered broker in open or static-auth mode.\nIt requires the broker's mode to match the registration, and a static connection must\nconfirm its current account and user identity. It reports the known first-party\nstreams and bucket backing streams as present or absent, the broker-observed\ntransfer-stream targets under a second exact-name grant in static mode, and the\nentries in the presence bucket. Both static connections must confirm their own\nidentity and the same broker server ID. A failed or incomplete native read refuses\ninstead of showing missing resources as absent. The snapshot is non-atomic and presence is not an\nauthoritative liveness or membership check. User-auth deletion inventory and apply\nare not supported. The dry-run removes no live or local state, and a read that covers its whole scope\nexits 0. A space that is prepared and not running, including one stopped with `cotal down`, has no\nlive broker to read or delete from: `space delete` refuses it with `TARGET_UNAVAILABLE`, exit 2,\nnaming the root to start it in, and `space forget` removes only its local record.\n\nOn a space this machine started, the apply refuses `LIFECYCLE_BLOCKED` while the space's own stack\nstill runs here, before any broker connection. On a controller root that is any service instance of\nthe space other than its broker, and on a legacy root any recorded daemon other than the broker.\nDeleting the streams under them leaves a stack that can neither start nor stop. The refusal names\nthe running services and `cotal down`, then `cotal reset store <space> --yes` on a controller root or\n`cotal clean store --force` on a legacy one to remove the stored streams. The dry-run shows the same\nrefusal as a warning.\n\nBare `space delete <name>` displays that impact on an interactive terminal and requires\ntyping the exact space name. A script supplies the exact positional name with `--yes`;\n`--json` without `--yes` refuses before a connection. After confirmation the command\nreads the broker again and refuses if the stream states, transfer names, presence\nidentities, registration, trust or broker identity changed. It opens a separately\nminted exact-target connection, checks each stream state again before the first delete,\nand submits one deletion at a time. A native acknowledgement is reported as deleted;\nprior absence is separate, and the receipt lists only the groups that have a name. It then checks each target's stream info and enumerates\ntransfer names again. A surviving or newly observed resource produces a partial\noutcome if deletions were acknowledged; an incomplete post-read stays unknown.\nA refused or unconfirmed delete stops the sequence without retry, reporting\nacknowledged names and refused, unknown or remaining targets. This is not atomic:\na live process can recreate a stream after the final read. The result\ndoes not claim that processes stopped, or remove registrations, credentials or local\nfiles. Open-mode impact and receipt label that any broker client can delete the space.\n`space forget` is not a substitute for live deletion.\n\n`use <space>` selects the space commands target when none is named. It changes only the selection:\nit starts nothing and does not check the broker, so a stopped or prepared space can be selected and\nthe command that targets it reports that. Selecting the selected space again is a `no-op`. A name that is not\nregistered is refused with `TARGET_NOT_FOUND`, exit 2, and the selection stays. Bare `use` reports\nthe selection. A selection that names a space no longer registered is reported with a\n`SELECTION_NOT_REGISTERED` warning by `use` and `space`, and stays until `use` changes it.\n\nA command run without `--space` targets the selection. When the selected space's broker is down it\nrefuses with `TARGET_UNAVAILABLE`, exit 2, and when the selection names a space no longer registered\nit refuses with `TARGET_NOT_FOUND`. Neither reaches another mesh.\n\nWith nothing selected, a command run in a project targets the space registered for that project's\nroot. A root with no registration whose `.cotal/` holds the stack journal `cotal up` writes is\nrefused with `TARGET_UNAVAILABLE`, exit 2, naming the broker the journal recorded and the root to run\n`cotal up` in. That is the state after an `up` that failed to start or a `space forget`, and the\ncommand never dials the default address instead. `status` there reads the machine, selects no space\nand probes no broker.\n\nWith no `--space`, `--server` or `--creds`, nothing selected and no project in the folder, a command\nthat several running spaces could serve asks which one when it runs in a terminal. The answer is the\ntarget of that one run, and the selection stays as it is; cancelling the question does nothing and\nexits 0. A script, a `--json` caller and `--no-input` are not asked: the command refuses with\n`TARGET_AMBIGUOUS`, exit 2, naming the running spaces. Every refusal of a target puts what to run next\nin `error.remedy`, apart from `error.message`.\n\nA manifest's `space:` is the target of native `up -f` and the new team\ncheck/boot/deploy routes. A `--space` that names another space is refused before\nanything is written. The old `spawn -f` manifest spelling no longer deploys;\n`team deploy` applies all-new live channel-only manifests or bounded\nstatic/open agent and mixed manifests. Reapply remains unsupported.\n\n`cotal meshes` still lists, registers and drops meshes. `space`, `space add`, `space create` and\n`space forget` replace it, and its `--json` lines are the old shape. A bare `meshes add` on a terminal is still the\nguided form.\n\n## Mesh registry\n\n```bash\ncotal meshes [--json]\ncotal meshes add # guided, on a terminal\ncotal meshes add <space> --server <url> [--root <dir>] [--mode auth|open|user] [--tls] [--allow-unencrypted-overlay] [--force]\ncotal meshes add <space> --mode user (--user-auth-file <bundle.json> | --from <https url>)\ncotal meshes rm <space> [<space> \u2026] [--force]\ncotal sync [--idp <auth base URL>] [--json|--plain]\ncotal status [--json|--plain] [--all] [-v|--verbose] [--space <s>] [--server <url>]\ncotal status --components [--space <s>] [--server <url>]\n```\n\nEveryday command forms use positional agent names: `cotal attach <name>` and `cotal stop <name>`.\nFor one-shot messages, choose one destination with `--to/-t`, `--channel/-c` or `--role/-r`:\n`cotal send -t reviewer \"Please inspect the patch\"`. Message receipts do not repeat the body.\nThey distinguish storage from delivery and say that no reply listener remains active.\n\nBare `cotal` and `cotal help` show a compact list of useful actions. Use `cotal help --all` for\nthe full command catalogue. `cotal cfg` is the shorthand for `cotal config`. Its default view\nshows effective values and sources; `config --verbose` adds the storage path and revision.\n\nStatus starts with This machine, groups registrations under Brokers, then shows the selected\nspace, this folder's maintenance journal under Maintenance when it holds one (its state, attempt\nand next step; `maintenance` in JSON), unsettled fresh and additive attempts in this folder when present, owned\nservice records and useful commands under Next. Connection state, local process state and service replies\nremain separate observations. Other registrations say Not checked unless `--all` reads them.\nUse `status --verbose` for installation paths, extension records and the detailed local diagnostics.\nThe default report does not include the agent roster; use `ps` or `who`.\nThe deployment-attempt section lists at most 100 fresh and 100 additive\nlocal IDs, their returned/unsettled state and unreadable paths, with full\nreadable coordinates in JSON.\nIt appears only when there is something to inspect. A missing fresh summary\ndoes not prove the original broker never started; an additive intent does not\nprove which resources changed. This read grants neither retry nor stop\nauthority. If the folder inherits\nthe machine's home `.cotal`, status warns that unqualified `up` refuses instead of starting\nthere. Prepare the intended folder first with\n`space create <space> --root .`; `up --root .` then names it.\n\nOwned services use the saved controller definitions and instance records for the selected space.\nFull instance IDs and execution hosts stay visible. Process observations and historical readiness\nare separate columns; neither a saved serving state nor a supervisor receipt becomes a fresh\nendpoint response or OS unit-state check. Legacy PID-file rows are labeled separately. On a root\nwhose controller stack serves the selected space, its members are the `stack-` rows here, the\nlegacy rows and the stored-state hint are omitted, and when the stack does not answer, Next offers\n`cotal up --root <root>`. Unreadable\nservice record families make the report partial instead of becoming empty lists. Warnings retain\na classified cause code, such as unsupported host observation, without native diagnostic bodies. These reads\nimport no provider and start, stop, adopt or repair nothing. `--all` extends broker checks; this\nlocal owned-service section retains its explicit selected-space scope.\n\n`meshes` lists the meshes this machine knows; a `*` marks the `current` default a bare\n`cotal spawn` joins. Entries learned from a signed-in account are marked `discovered`. Their\nregistration trust is stored under the account's private auth state, and the registry contains no\nsession token or sentinel credential bytes. Commands resolve the catalog `slug`; a different human\n`name` is rendered only as a label.\n\n`meshes --json` prints one JSON object per recorded mesh per line: `space`, `server`, `mode`,\n`root`, `default` (the `*`), and `origin` (`up`, `manual`, or `catalog` for a discovered entry). A\nlocal or hand-registered entry also carries `offline`. A discovered entry is never probed, so it has\nno `offline` field. `tlsRequired`, `events: \"required\"` and a discovered entry's `catalogName` appear\nonly when the record has them. An empty registry prints nothing and exits 0. The note about a default\nthat matches no record goes to stderr, so stdout carries only rows, on a first run too. The table is\npresentation and is not a stable parsing target. `meshes add` and `meshes rm` refuse `--json`.\n\nA registry record this build cannot use is refused by name, never rendered and never skipped. One\nthat does not parse, or is missing a field every consumer reads (`server`, `mode`, `root`, `ts`,\n`space`), makes every registry command exit 1 with the file's path and what is wrong with it.\nRemove the file or restore the record; nothing repairs or invents a field for you. A registry\ndirectory that exists but cannot be read, such as one without read permission or a regular file in\nits place, also makes every command that reads the registry exit 1 with its path and the read\nerror. Only an absent directory is an empty registry. A record removed between listing and reading\nis omitted from that listing. A keyed read also treats a record removed during its read as absent;\nparse, shape and other read failures still refuse by name. Recording or removing a mesh also sweeps\nthe directory for an older-format record of the same space. Any error listing the directory for\nthat sweep, other than an absent directory, fails the operation with that error.\nAn unknown `--space` names the missing registration and points to `cotal space`, which also\nlists prepared spaces.\n\nAn IdP may advertise a same-origin space catalog during login. Cotal reads the complete snapshot and\nadds every valid registration without a separate `meshes add`. A snapshot younger than five seconds\nis used without a request. After that, commands that resolve a mesh target conditionally refresh the\nsaved catalogs. An operation targeting a discovered space refreshes only that space's account and\nrefuses if that account fails. Operations targeting local or manually registered meshes refresh every\naccount, print one warning for each failure, and continue. `cotal status` refreshes every account,\nnever refuses on a refresh failure, and lists each account as `fresh`, `updated`, `not-modified`,\n`no-catalog`, or `failed` with its error. With no cached sign-in, catalog preparation reads without\ncreating a control directory or lock. An explicit IdP without a cached login refuses with\n`AUTH_REQUIRED`. An unreadable sign-in or catalog cache refuses with `DEPENDENCY_UNAVAILABLE`\nbefore refresh or registry application. Both exit 2 and leave the cache untouched; diagnostics\nname the path without printing cache contents or parser excerpts.\n`cotal sync` bypasses freshness and reports added, changed, removed, unchanged, and name collisions. `--idp` limits it to one signed-in account. It never connects\nto a broker. `--json` gives each account under `accounts` with its `idp`, `state`, `catalog`, the\nfive name lists, `selectionCleared` and `error`. An account whose catalog could not be refreshed keeps\nits last snapshot and is named in a `CATALOG_UNAVAILABLE` warning, so the result is partial, exit 4;\nwhen no account could be refreshed, `sync` refuses with `DEPENDENCY_UNAVAILABLE`, exit 2.\n\nThe registry is updated under the same lock that guards the catalog cache, so a command never lists\na discovered space set that another command is still writing. The cache records a fetched snapshot\nas not yet applied before the first registry write and as applied after the last. If a command dies\nor is stopped in between, the next command applies that snapshot again before it can use it, with\nno request inside the freshness window.\n\nThe shared dispatcher applies this preparation to every command that declares both `--space` and\n`--server` as mesh-target flags, including commands registered by other packages and commands that\ndeclare their own equivalent flag objects. Daemon and startup commands that use those names only as\nconfiguration explicitly opt out. Registry-local `meshes add` and `meshes rm` never refresh a catalog.\nWhile the registry holds a record this build cannot use, the preparation neither refreshes nor\napplies a catalog, so the command's own checks run first. A snapshot left unapplied is applied by the\nnext preparation after the record is restored or removed. A command that resolves its target through\nthe registry still refuses the record by name.\n\nRun on a terminal with the space or `--server` missing, **`meshes add` is guided**: it asks for the\none thing that cannot be derived (the broker URL), probes it, and tells you what answered - open or\nrequiring credentials. It then offers the spaces your `--root` already holds credentials for, states\nthe mode as a fact about that broker rather than asking, and shows the exact record before writing\nanything. A broker that does not answer, or a space name already registered, becomes a choice rather\nthan an error. Anything you pass on the command line is taken as given and not asked again. Without\na terminal - a script, an agent, CI - nothing prompts and the flag form's errors stand\n(`COTAL_NO_PROMPT=1` forces that too).\n\n`cotal up` and `cotal down` maintain their own records. `meshes add` registers a mesh they cannot\nspeak for: one running on another machine, a shared broker, a hosted space. `--root` is the folder\nwhose `.cotal/auth` holds that mesh's credentials and whose `.cotal/agents` holds its personas.\nThe default is the project you run it in. The registry stores that path, never a secret. `--mode`\ndefaults to `auth` when the root holds the space's account record and to `open` otherwise. The\nbroker is probed before anything is recorded, so a wrong address, or credentials that mesh will\nnot accept, fails here instead of at the first `spawn`; `--force` records without verifying (and\nreplaces an existing record).\n\nA hostname or public address is registrable only when the connection will **require TLS**. Pass\n`--tls`, or use a `tls://` URL. The scheme is recorded as enforced intent, so every later dial\nthrough the record demands the handshake (and `meshes add tls://\u2026` against a plaintext broker is\nrefused at registration). Without required TLS the fence admits loopback literals, and\nprivate-overlay literals only with `--allow-unencrypted-overlay`, which records your acceptance that\nthe overlay is protected only while its tunnel is up ([Run a mesh](run-a-mesh.md#which-addresses-you-may-register)). RFC1918 addresses are refused in both modes because a cafe LAN is private but does not belong to you.\n\nA **user-auth** mesh registers from supplied pinned trust, never guessed: `--user-auth-file`\ntakes the bundle exported where the mesh runs; `--from` asks before it dials the address at all,\nthen fetches the `/.well-known/cotal-mesh` discovery document under that address (HTTPS only; a URL\nthat already ends in that path is fetched as given), displays the pins, and asks again before\nadopting them. The document must parse as JSON under any content type the host serves it with; an\nanswer that does not is refused with the URL and the content type that came back. Neither fetch follows redirects: a 302 can move a pinned fetch\nonto plaintext or onto another host, so it is refused rather than followed, and the pinned\nexchange must itself be an `https://` URL, except for an exchange on this machine, where plain\n`http://` is accepted for a loopback *literal* (`127.0.0.1`, `::1`, any spelling of them) but not\nfor `localhost`, which is a name rather than an address. Registration verifies that the exchange\nanswers `/health` and `/jwks` as the pinned issuer. It also verifies that the broker refuses a bare\nconnect; that auth-required refusal is the pass. The sentinel credentials land in a 0600 file under\nthe entry's root; the registry records only the path.\n\n`meshes rm` drops records. It never stops a mesh. For a mesh running on this machine `cotal down`\nis the right verb, and `rm` says so unless you pass `--force`. A broker record that cannot be proven\nstopped counts as running, as it does for `clean`. A hand-added record is removed by\n`meshes rm`, by an `add --force` replacement, or by a `cotal up` that actually starts the broker for that same space, server and root, which becomes that\nmesh and so takes the record over (a `cotal up` for that space anywhere else refuses instead).\nNothing that merely *infers* a record is stale from a dead broker touches it: an\nunreachable broker is listed `offline` and stays, whether `cotal up` or `cotal meshes add`\nwrote the record; a foreground `up` whose broker exits unexpectedly keeps its record the same way.\nA bare command does not treat that offline record as a running mesh;\nname it with `--space` to restart it. When a bare control command resolves its target, it probes the\nrecorded brokers once, and its mode check and its connect both read that one result. `cotal down` / `cotal clean all` still drop an `up` record for the project\nthey tear down; a hand-added one they leave alone even when it shares a root, because nothing\non this machine could write it back.\n\nA discovered entry belongs to the normalized IdP origin and proved subject that supplied it. Local\nteardown, cleanup, and liveness pruning do not remove it. A manual or locally started entry with the\nsame name wins and remains untouched; that discovered name is reported as a collision. Logging out\nremoves only the discovered entries owned by that account.\n\n`cotal meshes` and `cotal status` print `events: required` for a registration carrying\n`policy: { events: \"required\" }`. On that space, foreground spawn, detached spawn and manager\nstarts that host a native harness cannot opt out or launch without an event plane. `--no-events` is refused with\nthe space named. A connector without an event plane is refused with both the space and connector\nnamed. A session whose own grant omits `events.<owner>.<actor>` is refused before joining and the\nmessage names a full-row `actor grant` repair. A running seat whose event plane stops keeps running\nand keeps its mesh connection on that space as on any other; the connector re-establishes the plane\nitself and reports every failure with the delay before the next attempt. A seat the manager resumes\nfrom a preserved cut meets the same rules as a fresh\nspawn: a seat retained with `--no-events`, or on a connector without an event plane, is refused, and\na resumed seat carries the requirement in its launch material.\n\n`join`, one-shot sends and passive protocol views host no native harness turns. They publish no\nAG-UI runs, regardless of who drives their input. Required events do not turn terminal messages\ninto agent runs or copy channel and DM text into an event stream. This launch policy is not an\nadditional broker authorization boundary. The broker still enforces each endpoint's grants.\n\nThe selection `cotal use` sets applies from every directory, including inside another mesh's\nproject. `status` is a read-only report in this order: this machine\n(the installed `cotal-ai` version, NATS and each named integration), the installed extensions and\ntheir versions, this folder's `.cotal/`, the recorded meshes, the selected mesh (its connection, the\ndelivery responder, its channels and membership feed), then the hints. It lists no roster; `cotal ps`\nlists managed seats. Stale Claude skills and out-of-date `.agents` skills recommend\n`cotal setup --skills`, not unscoped `cotal setup`. `status` takes `--space` / `--server` to pick the\nmesh to inspect; it starts nothing.\n\nStatus displays broker addresses without URL user information, query parameters or fragments,\nincluding component facts. Saved addresses and connection inputs are unchanged. An address that\ncannot be displayed safely is `null` in JSON, `unavailable` in text, and names a\n`BROKER_ADDRESS_INVALID` warning with a partial result. Its broker component is refused.\n\nOnly the selected mesh is checked. Its row in the recorded meshes dials its address with its\nrecorded TLS requirement, and every other row reads `not checked`. `--all` checks every recorded\nmesh, including discovered ones, each with its own recorded TLS requirement.\n\n`status --json` writes one envelope whose `target` is `{space, scope}`, where `space` is the selected\nspace or `null` and `scope` is `selected` or `all`. Its `result` is the report the text draws:\n\n| Field | Holds |\n|---|---|\n| `kind` | `infrastructure` |\n| `machine` | `version`, `install` (`kind` and `root`), `nats` (`path`, `bundled` or `missing`), `integrations` (each `{name, state, text}` a connector's setup reports), `connectors` (each `{name, requires, missing}`), `skills` (`state`: `current`, `not-installed`, `behind` or `unreadable`), and `web` (`installed`, `url` and the error of each when it could not be read) |\n| `extensions` | `root` and `items`, each `{package, label, version}` |\n| `folder` | `root`, `auth` (`state`: `space`, `spaces`, `none` or `unreadable`), `personas` (`root`, `default`: `valid`, `invalid`, `missing` or `unreadable`, `demo` and `error`), `launches` (whether a bare spawn launches from this folder's catalog; `null` when no mesh resolves), `processes` (each `{name, state, pid, detail}`, `state` one of `running`, `stopped`, `not-serving`, `unknown` or `unreadable`) and `storedState` |\n| `meshes` | `current`, `unreadable` (the registry directory, a record or the current-mesh pointer that could not be read, or a pointer that is empty, each as `{file, error}`), `items` (each with `space`, `server`, `mode`, `root`, `origin`, `tlsRequired`, `eventsRequired`, `current` and `check`: `reachable`, `unreachable` or `not-checked`) and `catalogs` (each `{idp, state, error}`) |\n| `selected` | `{resolved: false, code, message}` when no mesh resolves (`code` is `registry-unreadable` when the registry directory or a record could not be read); otherwise the target's fields, `connection` (`ok` or the reason it failed; on a user-auth mesh `reachable` or `unreachable`), `login`, `deliveryResponder` (`bound`, `unbound`, `stale` or `unknown`), `snapshot`, `channels` and `membershipFeed` |\n| `services` | Selected-space local owned records: `space`, `state` (`read`, `partial` or `not-checked`), non-atomic `observedAt`, public definition/instance projections and explicit scope. An unreadable family is `null`; stored readiness is historical. Older serialized snapshots may omit this observation, which is not an empty inventory. |\n| `hints` | The hints, in the order the report prints them |\n\nAn unreachable broker, a stopped process or an unbound responder is a state the report names, and\n`status` exits 0. A part it cannot read is named on its row and as a warning, and the run is\n`partial` with exit 4: a PID record, the folder's trust material, the delivery-aware marker, the\nconnectors, the skills bundle, the web records, a persona catalog, the mesh\nregistry or one of its records, the current-mesh pointer, a user-auth login, or the selected mesh's\nchannels or its owned service definition/instance records. Only an absent catalog, registry or pointer reads as empty or unset; an empty pointer is\nnamed too. A registry directory or record that cannot be read also leaves the selected mesh\nunresolved, because resolution refuses it, and the readable records are still listed. A corrupt or\nunreadable extensions manifest is refused while loading the command surface, before this report\ncan run. The manager\nrow asks the service endpoint once: a live process that does\nnot answer is `not serving`, and a probe that could not be made leaves the row\n`running \xB7 service unchecked`.\n\nThe local auth reader uses a stripped signer only when both split trust records are absent.\nThe load refuses dangling and looping links at either record even when an older stripped signer is present.\nA process row whose PID record exists but cannot be read, or exists under both its current and a\npre-upgrade name, reads `pidfile unreadable` with the error, and the other rows still print. A live\nmanager whose delivery-aware marker cannot be read keeps its row and names the failure as\n`delivery-aware marker unreadable` with the error. The `Web process` row\nprints the address the selected mesh's dashboard recorded once it was listening, while the PID in\nits `web.pid` is alive. The dashboard that holds `web.pid` picks the record. A dashboard started with\n`cotal service start` writes its session record beside its service startup receipt, and status finds\nthat record through the web service instance whose recorded process is that PID. Any other holder is\nread from `web.session`. Neither record is read in place of the other. A `web.pid`, session record or\nservice instance record that exists but cannot be read is named on the row with the error.\nOtherwise it reads `down`, or `not installed` without the web\nextension. Status and the `cotal setup` card count the web extension as installed only when a\npackage in the extensions manifest provides `web` and that package is on disk. A web package record\nthat cannot be read, is not JSON or has no `version` is named on the `Web extension` row and on the\ncard's web row, after the dashboard's address when it is listening and beside an unreadable\n`web.pid` or session record. The `Web process` row still prints, and reads `down` unless the\ndashboard is listening.\n\nIf a refresh fails, `status` may still show the kept catalog bytes for diagnosis. It labels them\nstale with the last successful snapshot timestamp and the refresh error. It never calls that state\nsynchronized or online. If a selected discovered space vanishes from a successful snapshot, the\nselection is cleared and the command reports that no default is selected.\n\nFor a user-auth mesh the selected-mesh section reports the login `status` works as: the signed-in\nsubject when this machine holds a cached session for the entry's pinned IdP, or the exact `cotal\nlogin --idp <url>` line when it does not, with no network round trip either way. A locally\nprovisioned space also shows the actor grant row; a discovered or registered remote entry reports\nthe grant as not checkable on this machine, because the ledger runs where the space was\nprovisioned. `--components` on a user-mode target probes as that same signed-in login (`ps`'s\ncredential), never a static mint; when the login cannot supply a credential, the row says why\ninstead of printing the broker's refusal of an unauthenticated probe.\n\nPersona rows name the catalog they describe. If this folder and the selected mesh use different\ncatalogs, status names both and marks which one spawn launches from. A green `default` means the file\npasses the same agent-file loader spawn uses; a present but invalid file is reported as invalid.\n\n`cotal status --components` prints the report, then a fail-loud per-component health pass. It takes\nno `--json`, `--plain` or `--all`. It reads **each\ncomponent's own control surface**, rather than treating a PID, a lease, or a successful probe of a\nsibling as proof that the component serves. It prints one of `serving`, `absent`, `not-serving`, or\n`refused` for each component and exits `0`, `1`, `2`, or `3` respectively (the highest observed\nstate wins). That exit replaces the report's own, so a partial report followed by a clean pass exits\n0:\n\n- **manager**: local PID record, its liveness-lease holder and PID, then the manager's own typed\n `status` service reachability from this host. Manager builds that do not report static\n reconciliation say `static reconciliation not reported by this manager build`; the line stays\n visible even when the manager is otherwise `serving`.\n- **delivery**: local PID record, its ready lease (`ready` is the daemon's own bound-control\n signal), and the latest `renewal.<spaceKey>.json` adoption verdict, the record of the space the\n command was asked about, keyed per space the way the pidfiles are. A re-signed credential and a\n broker-accepted adoption stay distinct facts. A root-only `renewal.json` left by an older build\n names no space and is never read as any space's verdict (`doctor auth` names it as a leftover).\n- **web**: local PID record, then the `/api/meta` response at the address the dashboard recorded\n once it was listening, which must name the same PID. The holder of `web.pid` picks the record, as\n on the `Web process` row: a service-run dashboard's own session record, found through its web\n service instance and named on the row as `service instance <id>`, or `web.session` for any other\n holder. The probe presents the readiness nonce recorded beside that address, the one credential\n the dashboard accepts on `/api/meta`. The probe follows no redirect. An answer with a status\n outside 2xx, a 3xx included, is `not-serving`, and the row names that status, such as `HTTP 401`.\n A live PID with no recorded address (the dashboard is still writing it, or an earlier build\n started it), or an unrecognizable process record, is `refused`, not a green default-port guess. A\n session record or service instance record that exists but cannot be read is also `refused`, and\n the row names the read error.\n- **broker**: the registered mesh URL dialed from this host with its recorded TLS requirement.\n\n`absent` means Cotal has no live local component record (or has a stale record); `not-serving`\nmeans the component record is live but its service/readiness surface did not answer or is not ready.\nThose are intentionally separate exit cases. A failed or unreadable probe is `refused`, never an\nabsent component or a clean zero. A PID record that exists but cannot be read, or exists under both\nits current and a pre-upgrade name, refuses only its own row. A record that its component removes\nwhile the pass runs reads as `absent`.\n\nEach component prints on one line. A control character inside a reported reason, such as a newline\nin a stored row's field name, prints as its `\\uXXXX` escape.\n\n## spawn\n\n```bash\ncotal spawn [<persona>] [--file <persona-file>] [-d | --foreground] [--dry-run] [options]\n```\n\nA bare `spawn` launches through the running manager, waits until the agent joins, and attaches this\nterminal to it (see [Managed launch view](#managed-launch-view)). It needs usable input and output\nterminals and refuses `--json` and `--plain`, before an identity is allocated. `--detach` (`-d`)\nreturns once the agent has joined, without attaching. `--foreground` runs the application directly\nin this process. `--file` (`-f`) names a persona file, never a manifest: manifests deploy with\n`cotal team deploy <file>`, and a manifest passed to `--file` is refused with that command.\n\nOn a user-auth mesh, an ordinary `--foreground` spawn without `--name` allocates the first free actor\nin the persona's series: `default_agent`, `default_agent_2`, `default_agent_3`, and so on. Held\nlocal material and existing grants are occupied, even when no presence is visible. Allocation\npreserves the existing session and its credentials. An explicit `--name`, a signed enrollment,\nand a managed handoff pin an identity and refuse a collision instead of renaming it. Authorization,\nnetwork and malformed-provisioning failures are not reasons to try another name.\n\nFor a foreground spawn onto a remote user-auth mesh, a one-time enrollment may stand in for a cached\nhuman login. Prefer a private file:\n\n```bash\ncotal spawn --foreground --file ./seat.md --space main \\\n --enrollment-file /run/secrets/cotal-enrollment\n```\n\nThe file contains only the enrollment URL, ending with at most one line terminator, and must be\nmode `0600` on POSIX. A launcher may name the same file in `COTAL_ENROLLMENT_FILE` instead, and run\n`cotal spawn --config ./seat.md --space main`. An orchestrator that cannot mount a file may set\n`COTAL_ENROLLMENT_URL`; that value is redeemed byte for byte, so a trailing newline in it is refused.\nNaming more than one source is refused, and so is `--enrollment-file` with `--detach`, `--creds` or\n`--on`. Enrollment input requires `--space` and applies only to a foreground persona spawn. If the\nmesh is not registered yet, the enrollment response must carry the stock user-bundle fields and the\ncommand needs a persona file (`--file`, or `--config` from a launcher) because there is no local\nremote-mesh persona catalog to read. The client\nredeems the URL once, registers the returned mesh material, exchanges the returned actor token at the\npinned auth service, and removes both enrollment variables before starting any child process. A\nregistered mesh whose record pins no exchange URL is refused before the URL is redeemed, so the\nenrollment stays unspent; re-register the mesh with `cotal meshes add <space> --from <url> --mode user`.\n\nA cached login for the same IdP and an enrollment are conflicting proofs, so the command refuses\nrather than choosing one. An invalid enrollment never falls back to login provisioning. Unknown,\nexpired, revoked, and already-used enrollments all produce one response: ask the owner for a fresh\none. See [Enrollment redeem](identity-and-auth.md#enrollment-redeem) for the HTTP contract.\n\nA runtime that starts a managed seat outside the manager's filesystem hands the child a managed\nhandoff instead: one `0600` file named by `COTAL_MANAGED_HANDOFF_FILE`, carrying the lifecycle the\nmanager already enrolled. The runtime builds the command with `delegatedSeatCommand`:\n\n```bash\nCOTAL_MANAGED_HANDOFF_FILE=/run/seat/handoff.json \\\n cotal spawn --config ./seat.md --space main --name <actor> --agent claude \\\n --expect-owner <owner> --expect-lifecycle-uid <uid>\n```\n\nThe `cotal` entry reads the file, deletes it and drops the variable before it parses flags, prints\nhelp or loads extensions, so every outcome leaves no file. The variable is read under any letter\ncase; spellings that name different files are refused after every one of them was deleted. The\nspawn then refuses a malformed handoff, or one whose space, owner, actor or lifecycle UID differs\nfrom `--space`, `--expect-owner`, `--name` and `--expect-lifecycle-uid`, before any broker\nconnection or exchange request. Every refusal on this path names the field and never a value from\nthe handoff. The registration's server, exchange and enforcement checks, the local state this\nmachine keeps for the space (its mesh record, user-auth state and agent secret files), target\nresolution, the policy refresh, the broker preflight and the agent auth preflight quote the space,\nthe server, the exchange URL, the actor or a path named for one of them in their own diagnostics and\nin the filesystem errors under them. For a handoff each prints one fixed sentence that names the\nfield and the phase instead, whether its check fails or an error is thrown. When the agent auth\npreflight's rollback then fails to remove a secret or file, that sentence is followed by the names of\nthe cleanup steps that failed, without their errors. The event-plane policy\nrefusals name the handoff's space field. An actor outside `[A-Za-z0-9_]` and a space that cannot\nname local state, such as `..`, are refused as malformed before any plane. A handoff conflicts with\nthe enrollment variables, `--detach`, `-f` and `--creds`, and needs `--config <persona-file>`. From\nthere it runs the enrollment consumer above without redeeming anything. See\n[Delegated seats](embedding.md#delegated-seats-outside-the-managers-filesystem).\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | resolved mesh | Target space |\n| `--server <url>` | registry entry | Broker URL override |\n| `--creds <path>` | none | Control-caller creds for an off-registry manager. Managed launches only; refused with `--foreground` and from a managed seat's own shell |\n| `--name <n>`, `-n` | persona's `name:` | Presence-name override (does not choose the persona) |\n| `--file <persona-file>`, `-f` | none | Persona file to launch, on the execution host. A value with no path separator that does not end in `.md` is read as `./<value>`. Refused together with a positional persona, and when the file is a Mesh manifest |\n| `--config <persona-or-path>` | none | Reserved for internal launchers that set `COTAL_MANAGED_HANDOFF_FILE`, `COTAL_ENROLLMENT_FILE` or `COTAL_ENROLLMENT_URL`; any other use is refused and names `--file` |\n| `--harness <connector>`, `-H` | persona's `agent:`, else the [`harness` preference](config.md#preferences), else `claude` | Connector type (`claude`, `opencode`, `jcode`, `hermes`, and so on). `--agent` is the legacy spelling; giving both is refused |\n| `--role <r>` | persona's `role:` | Role override |\n| `--model <m>`, `-m` | persona's `model:` | Model override |\n| `--variant <v>` | persona's `variant:` | Model variant override (connector-defined; e.g. OpenCode reasoning tiers) |\n| `--cwd <dir>`, `-C` | the serving manager's workspace root; with `--foreground`, this cwd | Working directory to root the agent at. On a managed launch, a relative path resolves against the manager's workspace root, not your shell, and a directory that does not exist on the serving manager's host is refused before launch. |\n| `--prompt <text>`, `-p` | none | Initial prompt auto-submitted at start |\n| `--opt <k=v>` | none | Connector-specific launch option, repeatable. Wins per key over the persona's `launchOptions:`; a connector with no option surface refuses it |\n| `--resume <id>` | none | Fork an existing session id into the mesh; only connectors that declare resume support accept it (see [the matrix](connectors.md)). The manager records the source session id, and `ps --wide` shows it. On a managed launch with `--on <instance>`, a Claude session held on this host is carried to that instance first ([Resume a session](connect-claude.md#resume-a-session)); carrying one needs `--on` |\n| `--no-events` | event plane on where supported | Opt out of the session's structured event plane (`--events` only restates the default) |\n| `--share-tools <sel>` | none | Share named operator MCP servers with the agent |\n| `--subscribe <a,b>` | persona's | Channel read-set override; `\"\"` clears it |\n| `--allow-subscribe <a,b>` | = subscribe | Read-ACL override; `\"\"` sets it to the read set |\n| `--allow-publish <a,b>` | deny | Post-ACL override; `\"\"` clears it |\n| `--detach`, `-d` | off | Launch through the manager, wait until the agent joins, and return without attaching (reattach with `cotal attach`) |\n| `--foreground` | off | Run the application directly in this process. Its exit, or the terminal's, may end the agent. Refuses `--detach`, `--creds`, `--on`, `--restarts`, and `--json` or `--plain` except with `--dry-run` |\n| `--on <instance>` | class anycast | Managed launches only: pin the launch to one manager instance id (the whole id, as `ps` prints it). Refused with `--foreground` (no manager to pin) and when empty |\n| `--restarts <n>`, `--restart-window <ms>` | none | Managed launches only, and given together: the manager restarts the agent at most `<n>` times within `<ms>`. Each must be a positive integer |\n| `--live-only` | off | `--foreground` only: skip the durable backstop |\n| `--enrollment-file <private-file>` | none | With `--foreground` and `--space`: a private file holding one seat's enrollment URL (see above) |\n| `--dry-run` | off | Preview the launch without allocating an identity, provisioning anything or starting an application |\n| `--expect-owner <u_\u2026>` | none | With `COTAL_MANAGED_HANDOFF_FILE` only, and required there: the owner the handoff must carry |\n| `--expect-lifecycle-uid <uid>` | none | With `COTAL_MANAGED_HANDOFF_FILE` only, and required there: the lifecycle UID the handoff must carry |\n\n`spawn` refuses `--runtime` and `--allow-stale` with \"manifest deployment options belong to cotal\nteam deploy <file>, not spawn\".\n\nEach session uses its connector's **event plane** by default: a stream of structured events\ndescribing what the agent did, rather than the prose it wrote, on a channel of its own. The channel is named after\nthe agent's principal, `events.<owner>.<actor>`, never after its display name, because two live\nagents are allowed to share a display name and would then share a stream. The launch grants publish\nrights on that channel alone, foreground and detached alike. On an open mesh, which issues no\ncredentials, the launch still allocates the agent an id, so the channel names a stable actor.\n`--no-events` is the explicit opt-out unless the selected registration says\n`policy: { events: \"required\" }`. Required policy makes the\nevent arm and grant mandatory, so `--no-events` and connectors without an event plane are refused.\n\nThe launch decision and the grant are separate on purpose. Holding publish rights on a channel is\nnot a request to publish to it, so writing an event channel into an agent file's `allowPublish`\ndoes not override `--no-events`.\n\nThe persona (`--file` or the positional name, else the [`persona` preference](config.md#preferences),\nelse `default`) is loaded from the target mesh's `.cotal/agents/` when it is a bare name. A reference that contains a path separator or\nends in `.md` is loaded from that file. A relative path resolves against the mesh root, except that\na managed handoff, and an enrollment onto a mesh this machine has not registered yet, resolve the\npersona file against the working directory. A missing\npersona is refused with the catalog directory or the file that was checked. A missing `default`\npersona also names the command that writes one: `cotal up` in the root of a space this machine\nstarted, or `cotal persona create default` for any other space. The launch flags\noverride the file. On a user-auth mesh the\neffective name is also the agent's actor token, so it must match the token grammar (no `-`); the\nspawn is refused with that explanation before any request is sent. `--foreground` runs the agent\nattached to your terminal; a bare spawn and `--detach` hand the launch to the running manager. Every\nmode gets the durable backstop on a mesh that runs the delivery daemon; `--live-only` skips it for a\n`--foreground` spawn (messages posted while it is disconnected are then not replayed). A foreground exit retires\nthe agent's creds and broker footprint, like a manager despawn. On a static-auth mesh, a spawn that\nfails after its footprint exists, for example because the creds directory cannot be written,\nretires that footprint the same way before it exits with the error. The creds and the footprint are\nremoved as separate steps, so a creds file that cannot be deleted does not leave the broker durables\nbehind, and the failed steps are reported together. On a user-auth mesh the two arms\ndiffer: a spawn against a mesh this machine provisioned revokes the actor row on exit, while a\nremote spawn (an enrollment or the advertised provisioning endpoint) removes only this machine's\ncredential files; its grant stays until the mesh operator revokes it, and the launch line says\nwhich arm you are on. A spawn through the advertised provisioning endpoint against a record that\npins no exchange URL is refused before the grant is requested, so no credential lands on this\nmachine. With no sign-in the spawn is refused with `AUTH_REQUIRED` (exit 2) before any request is\nsent. Any provisioning failure after the request, an endpoint 401 or 403 included, is reported as\n`OPERATION_UNKNOWN` (exit 3): the endpoint belongs to the deployment, and nothing says what its\nrefusal left behind. A foreground `--share-tools` selection that names a server the config does not declare is\nrefused before anything is provisioned. A managed spawn is an\n**action**: the manager accepts it and allocates the identity, and the CLI waits for a real outcome\n(the agent joins, exits, or about 30 seconds pass) before it reports or attaches (see\n[the control surface](control-surface.md)). See [Connect Claude Code](connect-claude.md) and\n[Agent files](agent-files.md). (`cotal start` was merged into `cotal spawn --detach`.)\nA managed spawn onto a manager from another Cotal release is refused before any request is sent\nwhen the manager's contract does not declare a field this CLI sends. The refusal names the field,\ncalls it version skew, and gives this CLI's version. A field you leave unset is not sent, so it\nnever causes that refusal.\n\nA manager has 50 seat slots, and each seat counts once. A slot is held by a managed seat (a row in\nthat manager's `cotal ps`, including a seat still joining), by a reserved launch the manager accepted\nbut has not started a process for, or by a cooling hold. A seat that ends within 10 seconds of\nstarting leaves its slot cooling until those 10 seconds pass, unless an operator stopped it. Such a\nseat holds only that cooling slot, even while its launch is still reporting the failure. A spawn\nrefused at the limit states that split and whether waiting can free a slot:\n\n```text\nat capacity (50 of 50 slots: 49 managed, 0 reserved, 1 cooling); waiting frees a cooling slot in 7s, or despawn one\n```\n\nA cooling slot frees at the stated time. A launch that has not settled frees its slot only if it\nfails, and a managed seat frees its slot only when it stops. The refusal counts a launch as pending\nonly while it holds a slot, so a launch whose seat already ended is not counted. The roster counts\npresence, which also includes peers no manager owns, so its total is a different number.\n\nRun from a managed seat's own shell on a static or open mesh, a managed `cotal spawn` launches as\nthat seat when it targets the seat's own space. Without `--space` it picks that target the way the\noperator path does, so a recorded mesh that is not running is skipped. The CLI reads the seat's\nlaunch identity (`COTAL_NAME`, `COTAL_ID`, `COTAL_LIFECYCLE_UID`, `COTAL_SPACE`, and on a static\nmesh the seat's own credential), so the manager records the seat as the spawner, the same as for\nthe seat's `cotal_spawn` tool. On a static mesh that credential also proves the seat's space, so a\nlaunch without `COTAL_SPACE` still runs as the seat, and a target space holding no credential for\nthe seat is refused. An open mesh acts as the seat only when `COTAL_SPACE` names its space. The\nseat can then stop the child with `cotal_despawn`, and the manager stops the child when the seat\nexits. On a static mesh a seat whose agent file lacks `capabilities: [spawn]` is refused, because\nits credential holds no spawn subject.\n`--on <instance>` keeps its pin: the seat's own credential has no instance route, so on a static\nmesh the CLI mints a one-shot `manager-caller` view for the seat, pinned to that instance and\ncarrying the spawn subject only when the seat's credential holds it. On an open mesh the call keeps\nthe TLS requirement the mesh records. A managed spawn from a seat's shell never falls back to the\noperator: `--creds` is refused there, and so is a target the seat cannot act on as itself, such as\n`--server` with an unregistered `--space`, a user-auth mesh, or an open mesh that `COTAL_SPACE` does\nnot name.\n\nForeground user-mode launches serialize their local actor material. A duplicate refuses\nwithout replacing the first launch's files. A new local-user foreground launch also refuses\nan existing grant before rotating it. Departure cleanup is conditional on the launch's token\nand lifecycle generation, so an earlier launch cannot revoke a successor's grant. Remote\nprovisioning remains the mesh operator's authority and can reissue material before the\nclient observes an existing same-host consumer.\n\n## models\n\n```bash\ncotal models [--json | --plain] [-H | --harness <connector>] [--refresh] [--on <manager-instance>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--harness <connector>`, `-H` | all registered applications | Application whose catalog to list |\n| `--on <manager-instance>` | native selection | Ask only the named manager instance. An unavailable or unauthorized pin refuses without choosing another. |\n| `--refresh` | off | Ask the connector to refresh its provider cache |\n| `--json` / `-j` | off | One JSON document: `result.items` holds each catalog as returned; the requested manager ID, answering ID and answering epoch are separate |\n| `--plain` | off | The same report without color |\n\nAsks the selected manager for each application's model catalog (model ids plus their variants)\nfor connectors that expose one. OpenCode and Codex query harness/provider surfaces; Jcode reads\nproviders that enable `model_catalog = true` in the operator Jcode `config.toml`. Jcode's listed\neffort tiers render as `<tiers> (declared, not provider-verified)`, and launch can still refuse one.\nA connector without a catalog says so with `supported: false`, and one whose install has no\ncatalog it can list, such as OpenCode 2.x, gives the reason in `unavailable`; neither is a failed\nread. A connector whose catalog could not be read, or that the manager could not load, makes the\nread `partial` (exit 4) with one `MODEL_CATALOG_UNREADABLE` warning naming its cause. The\nscreen points to that warning without repeating it inline. The remedy is to correct the stated\ncause before retrying; a refresh cannot repair missing connector configuration. A manager-level\nrefusal is reported separately. A connector whose harness the manager did not find at boot\nreports the reason boot recorded, as a spawn does, so restart the manager after installing it. Pick\na result with `cotal spawn --model <id> --variant <v>`, where `<id>` is the model id as the catalog\nprinted it. OpenCode and Codex ids are the full\n`provider/model`; Jcode ids are bare (`opus-5`, not `cliproxy/opus-5`), because the provider is\nselected by the operator's Jcode config and a prefixed id is refused at launch with the bare form\nnamed.\n\n## endpoints\n\n```bash\ncotal endpoints [--space <s>] [--server <url>] [--creds <path>]\n```\n\nLists the mesh presence roster: agents, the manager, and any other protocol endpoint, with each\nendpoint's role, kind, status, and current activity. Unlike `ps`, this is a read-only presence view;\nit is not limited to child processes owned by the manager.\n\n## who\n\n```bash\ncotal who [--waiting] [--all] [--json | --plain] [--space <s>] [--server <url>] [--creds <path>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--waiting` | off | Only rows that are waiting or report a condition |\n| `--all` | off | Also list infrastructure endpoints such as the manager |\n| `--json` / `-j` | off | One JSON document: `result.items` holds one row per participant |\n| `--plain` | off | The same report without color or marks |\n\nReads the live presence roster of the selected space. Each row has the participant's name, role, kind, status and condition,\nactivity, attention mode, harness, model and host, as its presence reports them. Infrastructure\nendpoints are hidden unless `--all` asks for them; the report says how many it hid.\n\n`managed` says whether a manager runs the seat. With no manager on the roster it is `false` for\nevery row and no manager is asked. Otherwise every registered manager is asked for its seats, and a\nseat is matched on its lifecycle UID. In a user-auth space the manager answers only for the\ncaller's own seats, so other seats show `managed: null` with a `MANAGED_SCOPE` warning. An\nidentity the broker does not let ask the managers, such as a user whose grant has no `admin`, sees\nevery seat `null` with a `MANAGERS_DENIED` warning. These two warnings say how far the identity's\ngrant reaches; the roster itself was read in full, so they do not make the run partial. A manager\nthat does not answer leaves every unmatched seat `null` with a warning.\n\nA part `who` could not read makes the run `partial` (exit 4): a roster snapshot that did not finish\n(`ROSTER_INCOMPLETE`), a roster that has gone stale (`ROSTER_STALE`), an error on the roster watch\n(`ROSTER_ERROR`), or a manager census it could not read in full (`MANAGERS_UNREAD`, and the warnings\n`ps` names). A credential that may not read the roster is refused with `PERMISSION_DENIED` (exit 2),\nnever shown an empty roster.\n\n`who` watches the roster and the channel registry on every mesh so it can report those states. On a\nstatic-auth or user-auth mesh the credential `who` connects with may not delete its watch consumers,\nso each one stays on the broker for five minutes after `who` exits. A script that runs `who` in a\ntight loop there can reach the broker's limit of 1000 consumers per stream.\n\n## whoami\n\n```bash\ncotal whoami [--json | --plain] [--space <s>] [--server <url>]\n```\n\nReports the identity this CLI acts under in the selected space and what it may read, post and do.\nIt reads the mesh registry, the login cache and, when it is on this machine, the user-auth grant\nledger. It does not refresh the remote space catalog or contact the IdP. A one-shot command has no\nsession, so there is no unread count or connection state to report.\n\n| Mesh | `identity.kind` | What it may do |\n|---|---|---|\n| Open | `open` | Everything; the broker authenticates nothing |\n| Static auth, signer on this machine | `space-signer` | Everything; each command mints its own short-lived credential for its task, so there is no standing principal |\n| Static auth, no signer here | `no-credential` | Nothing without `--creds <file>` |\n| User auth | `user` | The signed-in user's `cli` actor: `read`, `post` and `capabilities` are its ledger row |\n\nA user who is not signed in has no principal. A signed-in user with no ledger row (`grant:\n\"not-granted\"`) may read and post nothing. When the grant ledger is on another machine the grant\ncannot be read here: `read`, `post` and `capabilities` are `null`, the run is `partial` (exit 4) and\nthe `GRANT_NOT_LOCAL` warning says so. A remote registration can hold a cached login without the\nsecret that derives its owner and principal. Those fields stay `null`; the report calls the\nprincipal unknown on this machine, rather than calling the user signed out. The cached expiry is\nadvisory and the command does not verify the login with the IdP. `dms` says whether the identity\nmay read DM history: on an open mesh, or with the space signer's audit credential on an authenticated mesh. The report\nreads local configuration and does not contact the broker to check its authentication requirements.\nAn unregistered `--space` is refused even with `--server`. Server addresses in the identity report\nand connection refusals omit URL user information, query strings and fragments, and escape terminal\ncontrols. Addresses need a host and a supported broker scheme. TCP addresses cannot have a path;\nWebSocket routes and bare host addresses remain supported. Invalid addresses make `whoami` refuse\nrather than print the raw value. Recovery instructions use a `<url>` placeholder rather than a\nsecret-stripped connection address.\n\nBoth reads refuse an unusable local mesh registry with `SCHEMA_INVALID` (exit 1) before\nconnecting. The diagnostic names the file without repeating its stored field values.\n\n`endpoint invoke <target> <command> --cast` uses the native one-way verb. Only an ephemeral\ncommand can be cast; journaled commands and `--wait` combinations refuse before publication.\nThe receipt records the original request, resolved binding and broker flush, not a responder or\nconfirmed execution. A class cast is not retried or repaired if another instance refuses its\nbinding. A broker publish denial remains a refusal. A lost publication outcome remains unknown.\n\n## service-endpoint\n\n```bash\ncotal service-endpoint approve <proposalId> [--caller-ceiling a,b] [--request-id <hex32>] [--space <s>] [--json | --plain]\ncotal service-endpoint refuse <proposalId> [--request-id <hex32>] [--space <s>] [--json | --plain]\ncotal service-endpoint proposal <proposalId> | operation <operationId> | name <endpoint> [--space <s>]\ncotal service-endpoint name-bind <endpoint> --owner <u_\u2026> --claim <ref> [--request-id <hex32>] [--space <s>]\n```\n\nDecides and inspects service endpoint registrations on a user-auth space whose auth service runs\nwith `--service-endpoints`. Every subcommand is one request to the service's\n`/service-endpoint-host` route as the signed-in user: the route verifies your IdP token and derives\nthe granter itself, so no command names one. `approve` and `refuse` are the owner's: the command\nreads the proposal and its name first and decides at those revisions, so a proposal or name that\nmoved in between is a conflict the host reports, never a decision on something else. `proposal`,\n`operation` and `name` read as the owner: another owner's record reads absent. `name-bind` is the\noperator's: it needs the auth service on this machine and runs on its loopback face under the\nexchange capability, with your signed-in subject as the audit id. The host verifies no domain\ncontrol, so `--claim` is your own reference for the namespace you checked. The answer is the host's\nown. A decision or a binding is recorded under its request id in this root's `.cotal` state before\nit is sent, and the command prints that id, or names it in the failure when no answer came back.\nRun the same command again with `--request-id <hex32>` to send the recorded request as it was,\nwhich the host replays with the same approval or binding; a fresh read would carry the revisions\nthe first attempt moved, so the replay never rebuilds the request, and it takes no\n`--caller-ceiling`, `--owner` or `--claim`. An id this root never recorded, or one recorded for a\ndifferent command or subject, is refused with nothing sent. A run without `--request-id` is a new\nrequest, and a proposal already being approved answers `approval-in-progress`.\n\n## Handle inspection\n\n```bash\ncotal auth handle inspect <private-file> [--space <space>] [--json|--plain]\n```\n\nThis reads one protected native handle artifact or a bounded array of artifacts once. Without\n`--space`, it shows unverified public scope, holder and declared expiry claims. Signatures,\nissuer authority, parent links, holder currency and revocation remain not checked. No broker\nis contacted or credential issued. Signature bytes and original signed artifacts are never printed.\nThe human report shows at most 50 scope rows and names omissions; JSON retains public metadata.\n\nAn explicit `--space` selects online inspection on a registered static-auth space whose root\nholds matching native operator authority. One ephemeral provisioner reader is minted without\npersisting it. Its broker account and identity must match before leader-served signer and\nissuer-namespaced revocation reads. The inspector checks signatures, issuer scope, presented\nparent digests and containment. Every required sturdy-handle status must be explicitly unrevoked;\nmissing, malformed, unreadable or stale spec/status pairs fail closed. Reads share one bounded\nchain budget. No records are created or repaired, and no alternate signer or broker is tried.\n\nThese non-atomic observations are signature inspection, not a usable presentation context.\nDeclared expiry, presenter identity, holder/process currency and contract-dependent use authority\nremain separate. No holder credential is conferred and no handle is redeemed. Open and user-auth\nspaces refuse this static reader rather than borrowing an operator identity. Reader-close\nuncertainty is reported separately.\n\n## Account signer export\n\n```bash\ncotal auth signer --out <private-file> [--space <space>] [--force] [--json|--plain]\n```\n\nThis exports the selected space's existing data-account signer through the native stripped-signer\nformat. It does not create a caller, rotate a key, start a service or export the operator root.\nA cached login is not signing authority. The selected root must hold validated static trust;\nconflicting current-folder and selected-root account authorities refuse before publication.\n\n`--out` is required. Its parent must exist and pass the protected physical-directory checks.\nManaged `.cotal` and workstation state are not valid explicit export destinations. The file is private, and secret contents\nnever appear on stdout, in the receipt or in diagnostics. Keep this account-wide signer out of\nlogs and support bundles; it is not a least-privilege endpoint credential.\n\nAn existing path refuses by default. `--force` first inspects an owned private regular file and\naccepts only the stripped format for the same space and account. It refuses full trust bundles,\nother accounts, symlinks and concurrent changes. An identical export is a no-op. New files use\nnative create-only private publication; replacement uses the inspected snapshot under a writer\nlock. An unconfirmed publication remains unknown and is not retried. Platforms without the\nprotected file policy refuse rather than substituting a weaker writer.\n\n## Endpoint authoring\n\n```bash\ncotal endpoint init reviews [--dir <directory>] [--json|--plain]\ncotal endpoint check <contract-file> [--json|--plain]\n```\n\n`init` creates `contract.json` and an executable `handler.mjs` template. The default directory is\nthe supplied name; `--dir` chooses another one. Its parent must exist. Files are created\nexclusively, without following a pre-planted symlink or replacing an existing file. A partial\nwrite retains the named created material for review rather than deleting it automatically.\n\nA simple scaffold name such as `reviews` declares `com.example.reviews`. A full reverse-DNS name\nis kept. Choose a namespace you control before requesting serving authority. Scaffolding creates\nno service definition, reference, credential, registration or process. The handler accepts one\nJSON object with `command` and `args` on stdin and writes one JSON result on stdout.\n\nThe version-1 local contract file has `schemaVersion`, `endpoint` and `cluster`. The cluster has\nnative `urn`, `revision`, `attributes`, `commands` and `events` fields. Each command carries its\nnative declaration plus `input` and `output` schema bundles, each `{root, members?}`. The installed\nnative compiler derives their closure digests; authors do not hand-maintain a second digest next\nto an edited schema. `members` maps native digest references to supplied JSON documents. Unknown\nwrapper or command fields are refused. This file is local authoring input, not a new wire kind.\n\n`check` reads at most 1 MiB, validates the native cluster declaration, compiles every input/output\nwith the installed schema profile and reports the derived cluster closure. All schema references\nmust resolve inside their bundle; no URL or adjacent file is fetched. Attribute and event\nsemantics remain opaque to the native parser, so a file declaring them produces a partial result,\nexit 4, rather than a claim that those semantics were checked.\n\nNeither command contacts a broker, loads the handler, starts a service or proves serving\npermission. Authoring takes no space, broker, caller or invocation flags.\n\nServe the edited contract through the independent service controller:\n\n```bash\ncotal service add reviews --preset exec-endpoint --space <s> --root <absolute-root> --contract <absolute-contract.json> --handler <absolute-node-executable> --handler-args '[\"<absolute-handler.mjs>\"]'\ncotal service start reviews --space <s>\ncotal service status reviews --space <s>\ncotal service logs reviews --space <s>\ncotal service stop reviews --space <s>\n```\n\nThis preset supports Linux and an existing registered static-auth root. The executable is an\nabsolute protected regular file, with argv supplied separately. There is no shell, PATH lookup,\nimplicit installation, account switch or open/user-auth credential fallback. For a Node handler,\nselect the Node executable and pass the script path in `--handler-args`. That JSON array has at\nmost 64 fixed nonsecret strings and its serialized form has at most 8192 characters. The handler\nruns in the contract directory with an empty environment. It is trusted local code with the\noperator's OS permissions, not a sandbox; it must not daemonize or leave independent children.\n\nStart recompiles one protected contract snapshot and pins its cluster closure and the executable\nfile identity across the controller handoff. Changes before launch refuse; changes to the\nexecutable before a later call also refuse. Only ordinary untargeted ephemeral commands are\nsupported. Targeted, journal, trait, attribute and event declarations refuse before registration.\nThe host publishes the schema roots, members, closure manifests and cluster to the native\ncontract store, registers one fresh native instance, and uses the native issuance gate and\nbranded serve grant. The handler receives no signer, executor credential or broker environment.\n\nThe native serving boundary validates input before spawning and output before replying. Each\ncall receives one `command`/`args` JSON object on stdin and must return one UTF-8 JSON value on\nstdout with exit code zero. Calls and casts need a native deadline; the handler budget never\nexceeds it or 120 seconds. At most 32 handlers run concurrently. Input is bounded to 64 KiB,\nstdout to 256 KiB and discarded stderr to 64 KiB. Deadline, pipe, process, JSON and schema failures\ndo not replay effects. Handler payloads and stderr are not forwarded to service logs.\n\nInitialization is recorded only after the real serve subscriptions flush and the native ready\nstatus returns. Controller readiness remains a process-bound initialization observation, not an\nongoing endpoint health probe. Logs and private launch records name the full native instance;\nuse that instance for an attributed `endpoint describe` or `endpoint invoke`. Normal renewal\nuses the same issuer, identity and registration, with one immediate same-broker reconnect. An\nunconfirmed renewal withdraws the service rather than starting a replacement.\n\nStop first prevents new handlers, requests termination of its owned handler groups, waits for\nprocess/pipe closure, drains serving and closes the connection. Native deregistration pins the\noriginal registration revision and preserves successor or in-flight governance state. Private\ncreate-only intent, registration and removal records retain the original coordinates. A later\nlaunch of the same local instance refuses an unsettled or malformed attempt instead of silently\nrepeating it. Unconfirmed initialization or cleanup remains unknown; neither process disappearance\nnor an initialization receipt proves native cleanup. The separate `endpoint deregister` route\nretains its broker-gone and revision guards.\n\n## Endpoint control\n\n```bash\ncotal describe <endpoint> [--on <instance>] [--space <s>] [--server <url>] [--creds <path>]\ncotal invoke <endpoint> <command> [--args '<json>'] [--timeout <ms>] [--space <s>] [--server <url>] [--creds <path>]\ncotal invoke <endpoint> <command> --name <agent> [--admin] [--space <s>]\n```\n\nThe generic v0.4 service surface. `describe` resolves a registered endpoint's command set off the\nwire - the reserved `describe` command answers the registered contract digests, the schemas are\nfetched from the space's content-addressed contract store, recompiled, and verified against those\ndigests - and prints each command with its capability class and targeting shape. `--on <instance>`\npins `describe` to one manager instance's rail (the whole id, as `ps` prints it under its\n`manager <id>` headers), so an operator can read what that instance serves in a multi-manager space;\nunpinned, the class queue answers and the attribution line names whichever instance did. `invoke`\ncalls one command by name: `--args` is a JSON object validated against the fetched input schema\n*before*\npublish, and `--timeout` sets the reply deadline in milliseconds (default 10000). `--server` overrides the\nregistry's broker URL and `--creds` names a creds file for an off-registry connection. A targeted command takes `--name <agent>` (resolved to the agent's current principal through\n`inspect`) or `--self`. `--admin` uses the admin instrument credential, whose cross-agent reach rides\nthe operator-only `any` authorization mode. Neither command has compile-time knowledge of any\nendpoint's schemas - this is the same trust chain every built-in control command now uses. Needs an\nauth mesh: the manager registers its service on both static and per-user meshes (a signed-in user\nrides their bearer; each visible or invoked command still requires its existing grant, and cross-agent\nreach needs the `admin` scope). An open mesh has no service registry.\n\nOn a static-auth root the operator's own credential sends requests to the manager endpoint only, so\nthe broker refuses its `invoke` of any other endpoint before the call runs. When the call and the\nendpoint's whole command surface are untargeted synchronous ephemeral commands, the refusal names\n`cotal auth mint <name> --endpoint <endpoint> --space <space> --out <private-file>`, with the space\nthe call resolved and the `--server` it was given, if any, so the caller is issued for the space that\nrefused even when another space is current. Run the same `invoke` again with `--creds <private-file>`. An `invoke --on <instance>` of another endpoint is refused the same\nway and names no remedy: with `--on` the operator's credential still carries manager rows only, and\na paired caller file takes no instance pin.\n\n## Managed seats\n\n```bash\ncotal ps [--json | --plain] [--on <instance>] [--wide] [--slots] [--space <s>]\ncotal stop --name <n> [--on <instance>] [--space <s>]\ncotal attach --name <n> [--on <instance>] [--no-reconnect] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--name <n>` | none | Managed agent to stop / attach (required) |\n| `--on <instance>` | class anycast (`ps`: class scatter) | Pin to one manager instance id (multi-manager space); takes the whole id as `ps` prints it, not a prefix. An empty value (`--on \"\"`, an unset shell variable) is refused, never treated as absent. A roster principal id (`local.\u2026`) is refused with a message naming the instance id `ps` prints |\n| `--wide` (`ps`) | off | Print each seat as a labeled record that adds the operational facts the manager records: the provider the connector reported serving the model, `cwd`, `pid`, spawner, lifecycle uid, the owning manager's instance id and host, and for a `--resume` seat the session it forked (`forked from <id>`, with the source title and transcript SHA-256 once a Hermes or Jcode seat has recorded its fork; a carried Claude session prints `forked from <host>:<id>` with its title, SHA-256 and `carried <time>`, the time its bytes reached the manager). Model and requested variant stay in the identity row rather than printing twice. A fact the manager did not record (for example a runtime with no real process, or a connector that reported no provider) prints nothing, never a placeholder |\n| `--json` / `-j` (`ps`) | off | One JSON document (see below). Mutually exclusive with `--wide` and `--plain` |\n| `--plain` (`ps`) | off | The same report without color or marks |\n| `--slots` (`ps`) | off | List the durable static slot rows this manager owns instead of live seats, through the `slots` command. Mutually exclusive with `--wide`. A row that is not in the live roster still prints, with `LIVE` false; a retired row never prints |\n| `--no-reconnect` (`attach`) | off | End the attach when its session ends, instead of re-establishing it. For scripts that want one run and one exit code |\n\nOn a controller stack started with `--no-manager`, `ps` refuses `TARGET_UNAVAILABLE`: no manager\nruns, so there are no managed agents to list. Bare `cotal up` adds the manager.\n\nA raw `--creds` file is refused by `ps`, `stop`, `attach` and the other control commands, because\nthat route mints no endpoint-caller triple; the project folder, or `--space` against the registry\nentry, is the route that does.\n\nThe human `ps` report is presentation text and is not a stable parsing target. Scripts use `--json`,\none envelope whose `result` is:\n\n| Field | Meaning |\n|---|---|\n| `kind` | `seats`, or `slots` with `--slots` |\n| `items` | Each seat (or slot) row as its manager returned it, unchanged |\n| `managers` | Each manager instance asked: `{instanceId, state}`, where `state` is `answered`, `silent` (registered, no answer within the deadline, with the `liveness` this command established), `refused` (with its `error` and `code`) or `contract-mismatch` |\n\n`target` is `{space, scope: \"all-managers\"}` for the class scatter, and\n`{space, scope: \"manager\", managerInstanceId}` for `--on` or a user-auth mesh. A manager that is not\n`answered` makes the read `partial` (exit 4), with a `MANAGER_SILENT`, `MANAGER_REFUSED` or\n`MANAGER_CONTRACT_MISMATCH` warning naming it.\n\n`--slots --wide` is refused: `--slots` lists durable static slot rows, `--wide` prints live seat facts, and the two answer different questions. Across a multi-manager scatter, `--slots` prints each manager's rows under its own instance header, the same way the plain `ps` scatter does.\n\nA revision-32 manager also supports a bounded native `slots` query through\n`endpoint invoke manager slots --on <instance> --args '{\"names\":[\"worker\"],\"includeRetired\":true}'`.\nIt point-reads up to 100 distinct aliases and can return their retired slot/head and\ncleanup observations. The default `ps --slots` query is unchanged and excludes retired\nrows. Omitted aliases, live-map absence and stop acknowledgements are not retirement\nproof. Match the returned principal and lifecycle UID before interpreting any terminal.\n\nThese are operator clients over the running manager's control plane. The default row includes the\nconnector, model pin, optional requested variant, and runtime as operational descriptors for the\nmanaged row. They do not make a shared display name a unique protocol identity; use `--json` when\nunambiguous owner+actor attribution is required. An omitted variant means no override was requested;\nCotal does not invent an effective provider default it cannot observe. `ps` also prints two state\nfacts per managed agent, because they answer different questions: the process fact from the manager's\nown runtime handle (`running` with its uptime, or `exited` with how long it ran), and the mesh fact\nfrom the roster (`idle` / `working` / `waiting` / `mesh offline`, or `not in roster` when the seat has\nno presence row at all: a seat that has not joined yet, or one that never did). When the seat's\nconnector relays a harness-reported condition, the mesh fact carries its code and how long it has\nheld, so a seat whose turn died on a provider rate limit reads `waiting (rate_limit for 40m)` rather\nthan a bare `waiting`, and `--json` carries the whole `condition` object. When the connector reports\nthe seat's last work event (presence `activeAt`), the mesh fact ends with its age, such as\n`\xB7 active 3s ago`, and `--json` carries `activeAt`. A seat whose turn stopped advancing keeps\nheartbeating, so its presence row stays fresh and this age is what shows the stall. A seat can be\n`running` and `mesh offline` at once: the process is alive and its presence has lapsed. That row says\nhow long, as in `mesh offline for <age>` with an age such as `3.5h`, counted from the seat's last\npresence heartbeat, which `--json` carries as `offlineSince` (epoch ms). The age is read only from\nthe seat's own presence record, matched on its principal and lifecycle uid, so a same-named peer or\nan older lifecycle never dates it. The manager log names each managed seat that is offline on the\nmesh while its slot is held\n(`seat offline on the mesh: <name> - last heartbeat <time>; process <state>`), including one its\nwatch first sees offline after a reconnect, and each one that comes back\n(`seat back on the mesh: <name>`), so a watchdog that only checks process liveness has a line to\nact on. The manager does not reap or re-key such a seat. The mesh fact is only a verdict while the\nmanager's own presence watch is fresh: when that watch has been silent past the liveness window, or\nhas not replayed the bucket yet, every row prints `mesh unknown` with the reason instead (`--json`\ncarries it as `meshView: stale | unpopulated`), because `offline` and `not in roster` would then\ndescribe the manager's watch rather than the seat. The manager rebinds a watch that goes\nsilent under a live connection on its own, so `mesh unknown` normally clears within a liveness window.\nOn a user-auth mesh `ps` also renders each managed agent's last credential-refresh outcome, fail-closed.\n\n**Mode split (chosen up front, never try-scatter-then-degrade):**\n\n- **Static / open mesh.** Bare `ps` is a **class scatter**: it freezes the live manager class from\n the records registry, merges every registered instance's agents grouped and attributed per\n instance, and a non-answering instance is shown as `registered, no answer within the deadline`\n (never silently omitted). A refused list or a missing answer makes the census incomplete: rows\n from other instances remain visible, but `ps` prints an incomplete-census warning on stderr and\n exits 4 (`partial`), including with `--json`. Those rows are not a complete seat count. A contract\n mismatch prints one plain comparison of the requested and served input/output digest pairs and\n advises aligning manager versions. The no-answer label means only that the instance is registered\n and did not answer. It does not say the host is down, because a dead host never deregisters itself and a\n live one can be slow; if it is gone, deregister it.\n `--on <instance>` pins the read to one exact instance id instead. A wrong pin fails loud\n rather than falling through: a well-formed id that no live manager carries is reported as\n `manager instance <id> did not answer` (nothing else is asked), and a credential without that\n instance's rail is reported as refused by the broker, not as an unresponsive manager. A manager\n that answers with a refusal is shown with its own cause; \"no manager reachable\" is said only when\n nothing answered at all. If the scatter's own registry read fails (the freeze or the reconcile),\n `ps` says the manager registry could not be read rather than pronouncing on the managers, which\n may all be up.\n\n**The verdict is scoped to the endpoint rail the request rode.** An issued caller rides the\nversioned `ep.v1` rail, a separate subject space from the legacy `ep` rail, and an endpoint serves\nboth (SPEC 13.15). A manager older than the versioned rail serves `ep` alone, so it can be running,\nregistered and answering while an issued caller's request reaches nobody. Silence on `ep.v1` is\nreported as `no manager answered on the <rail> rail` with `ep.v1` as the rail, and names both causes\nit is consistent with: no manager running, or one older than the rail. The CLI cannot tell them\napart, because the service registry records no package version, so check whether a manager is\nrunning and, if it is, its version. The same scoping applies to `cotal run`'s hosted verbs, which\ndrop the `--local` suggestion there, since `--local` drives the run from the calling process and\nnames the caller as its answerer.\n\n**`stop` and `attach` route by seat locality.** A seat can only be stopped or attached by the\nmanager actually running it, and the class queue does not know which one that is. So on a\nstatic/open mesh both verbs first ask every registered instance which one hosts the named seat, then\naddress that instance directly. This happens by default; you do not need `--on`.\n\n`--on <instance>` remains the override, for when you already know where the seat lives or the\nlookup itself is degraded. On a **user-auth mesh**, the exchange selects one authorized manager\nfor a short-lived `manager-caller` view. `--on` requests a specific instance. Without it, a command\nrun from a managed seat's shell selects the manager that launched the seat (`COTAL_MANAGER_INSTANCE`),\nand any other caller's selection must be unique. Discovery and the command use that instance route.\nThe caller gains no registry read or scatter permission. An absent, ambiguous or unauthorized\nselection refuses before sending the command. With several of your managers live, the refusal lists\ntheir instance ids so you can pass one to `--on`. By default, when every manager you registered has\nexited, the refusal says to start one with [`supervise`](#supervise); the command is not sent to the local manager.\n\nA trusted hosted composition may opt into an [interactive local primary](embedding.md#interactive-manager-primary).\nUnder that policy, unpinned human control selects the genuine local Manager up front, even when your\nown remote Manager is live; an unavailable local Manager refuses without remote substitution. This\naffects all interactive Manager verbs, not only `ps`. Select your remote Manager explicitly for its\nspawn, run or terminal operations. Explicit pins and managed-seat launching bindings keep their rules.\nThere is no user flag for the policy, and stock `up` and recovery defaults do not enable it.\n\nA seat is reported as **not found** only when every reachable instance answered for itself. An\ninstance that stayed silent past the deadline, or that refused the read rather than answering, said\nnothing about which seats it hosts, so the seat may be running on it. That case reports that the\nlocation could not be established, names the instances that did not answer, and states outright\nthat it is not a report that the seat is gone. Read it as unknown and retry with\n`--on <instance>`; a retry loop that treats it as \"already gone\" stops looking for a seat that is\nstill running. A single manager cannot tell \"hosted elsewhere\" from \"does not exist\": it answers\n`not-found` for both, which is why the search asks all of them and why an incomplete search\nconcludes nothing.\n- **User-auth mesh.** `cotal ps` reports what **one** authorized manager knows about your agents\n (an instance-addressed read against its in-memory roster, owner-filtered). It does **not** report\n other manager instances or establish whether they are reachable. Completeness across a\n multi-manager user-auth space is not claimed.\n A manager that does not answer fails the command outright (`TARGET_UNAVAILABLE`, exit 2), rather\n than printing an empty list that could be read as \"no agents\". Your ledger row needs the `admin` scope to\n reach `ps` at all; `spawn` alone is refused by the broker (the ep tier boundary). A broker-refused\n read keeps that refusal and adds the documented scope requirement and `cotal auth grants` remedy.\n The broker does not identify whether the missing permission is for the route, command or target;\n the CLI does not substitute another caller or retry on a different route.\n\n`attach` streams and drives an agent's terminal on the `pty` runtime; detach with the escape key\n(Ctrl-] by default; see the [`detach-key` preference](config.md#preferences)). The key is recognised\nas the legacy control byte and as the kitty keyboard protocol and xterm modifyOtherKeys encodings of\nthe same press, so a terminal with either protocol enabled detaches too. It does so over a one-use, holder-bound\nmesh session ([SPEC](../SPEC.md) \xA713.6): the manager replies with a signed session grant (never a\n`127.0.0.1` URL), the CLI redeems it once over the broker, and the browser console (`cotal console`)\ndrives the same session. `stop` and `attach` need a running manager to talk to. On a static mesh\nthey are cross-agent admin operations. On a user-auth mesh, your own agents (any agent under your\nowner) need only the `spawn` scope; another owner's agent needs `admin` on your ledger row\n([identity & auth](identity-and-auth.md)). Launch detached agents with [`spawn --detach`](#spawn).\n\n**`attach` reconnects when the link dies.** A session lives on a network link, and a laptop that\nsleeps, a VPN that drops or a wifi handover kills it. When that happens `attach` prints\n`[cotal: connection lost, reconnecting]` on stderr and starts asking the manager for a new session:\na fresh grant, a fresh per-session credential, a fresh connection, so every attempt re-runs the same\nauthorization the first attach did. On success it prints `[cotal: reconnected]`, the manager repaints\nthe seat's current screen the way it does for any attach, and you carry on in the same terminal.\nRetries wait 1s, 2s, 5s, 10s, then 30s, for as long as the seat exists. The detach key is read the\nwhole time the loop runs, the waits and the attempts alike, so a reconnect never traps you: press it\nwhile a session is being established and the attach ends there, and a session that lands behind the\npress is handed back to the manager rather than left holding a slot. Everything else you type while\nthere is no session is dropped rather than queued, so keystrokes aimed at a terminal that turned out\nto be frozen, Ctrl-C included, are not delivered to the agent by a reconnect you did not know had\nhappened. That starts before the first session, not at the first reconnect: at a terminal, `attach`\nreads and drops what you type while it is still resolving the mesh, so a key struck at a prompt that\nhas not come up yet does not reach the agent when it does.\nThe terminal is in raw mode for the whole reconnect, including when the link died before the first\nsession finished opening, so the detach key works there too instead of echoing as `^]`.\n\nA **pipe** carries script input. For example, `printf 'ls\\n' | cotal attach --name web` is\nbuffered until the session opens. Buffering continues across reconnects, so\n`tail -f log | cotal attach --name web` does not lose the part of its feed written while the link was\ndown. Only a terminal gets the reader; `--no-reconnect` keeps the old behaviour on both.\n\nIt stops on its own when reconnecting cannot help, and says why: a manager that refuses the attach\nexits non-zero with the manager's own message, and a reconnect that finds the seat no longer there\n(despawned, or its agent exited while the link was down) exits cleanly with `seat <name> is gone`.\nIt stops the same way on a refusal the CLI raises itself, such as a broker that denies the request.\nA local connect refusal that retrying cannot fix, such as a static-auth mesh whose seed is now\nmissing, also exits non-zero with the refusal's own sentence. A broker that is still unreachable\nkeeps the loop trying in silence.\nA refusal that could still pass, such as a manager at its session ceiling, is relayed in the\nmanager's own words while the loop keeps trying, once per refusal rather than once per attempt.\nPressing the detach key ends the view with `Detached`. A remote terminal-session end, including\nprocess exit or manager shutdown, prints `Attach session ended`. Neither report claims the agent\nis still running or that its cleanup completed. Use `cotal ps` to check its current state.\n`--no-reconnect` restores the single-session behaviour.\n\nEach reconnect also hands the abandoned session back to the manager, over the first link that can\ncarry the message, so an attach that flaps does not eat the manager's session slots one outage at a\ntime. If that message never gets a link, the attach says so when it ends. The live-session ceiling\ndefaults to 64 concurrent sessions (`--max-sessions`); the browser console opens one session per\npane, so a dashboard over a large mesh should size for agents \xD7 panes. Hitting the ceiling refuses\nbefore a credential is minted and names `--max-sessions`.\n\nWhich mesh `attach` resolves also decides **how it redeems the grant**. On a registered open mesh\nthere is no local seed. The CLI connects bare, the same way other control commands already do, and\nthe session rail is the caller rail that a real open-mode connection already reaches. Telling the\noperator to re-register the root is false: the registered root is already the contract. On a\nstatic-auth mesh the grant is still redeemed by minting a short-lived\nsession-scoped credential from the seed at the root the mesh resolved to, never from a `.cotal`\nfound by walking up from whichever directory you happen to be standing in. The difference is not\nhypothetical: `~/.cotal` exists on every install because the mesh registry lives there, so a command\nrun anywhere under your home directory but outside a project used to mint from your home\ndirectory's trust and present it to a broker that trusts a different chain, which surfaced as a\nbare authorization failure that named nothing. A directory that does hold another chain for the\nsame space is now reported on the way past, and not obeyed:\n\n```text\n! this directory resolves to /Users/you, whose .cotal/auth holds a DIFFERENT trust chain for space \"team\".\n attach used /Users/you/projects/app, the root this mesh resolved to. The other one is not being used, and is worth a look.\n```\n\nWhen a **static-auth** mesh holds no seed at the resolved root, `attach` refuses and names what it\nresolved, the broker and the root, instead of describing a directory it did not use and instead of\ntaking the open-mode path. An authenticated registry entry with a missing seed is still\nauthenticated. On a USER-AUTH mesh `attach` reads no seed. It sends your login and the session grant to\nthe auth service, which issues a `session-caller` bearer only if your owner and actor hold that\nsession. The connection it opens expires with the session grant.\n\nTerminal bytes stream over the mesh; the manager's own HTTP/WS face serves the console. That endpoint binds\n**loopback by default**, so nothing is exposed by accident; `cotal up --host <addr>` passes its bind\naddress down, which is what lets you reach the browser console (`cotal console`) for an agent whose manager runs on another machine.\n`attach` does not use that face: it redeems a signed mesh session grant over the broker instead (see above), so it reaches a\nremote manager regardless of the bind address. A\nbare `cotal supervise` and an embedded manager stay machine-local. Set it directly with\n`supervise --console-host <host>`.\n\nThat address is **recorded on the mesh** and carried forward, because it is a decision rather than\nsomething later commands can work out for themselves (a broker dial address is not a manager bind\naddress). Every later manager launch for the same mesh reuses it, including a same-root `cotal up` repair\nand an adopted preserved or restored listener. A manager replacement\ndoes not quietly move a reachable attach face back to loopback. Passing `--host` again overrides it,\nso you can widen or narrow exposure whenever you like; a mesh that never asked stays loopback-only\nand records nothing.\n\nBecause that face mints terminal read and write authority for every managed agent's browser session, it is credentialed in two\ntiers. A mesh caller receives a **ticket** bound to the single agent the manager just authorized,\nsingle-use and short-lived, so one authorized attach can never be re-pointed at someone else's\nagent. The **console token** is the operator's own, reaches every agent, and is printed only to the\nmanager's output. The roster, the live feed, and the PTY stream all answer `401` without one; the\nstatic console shell is served openly, since it describes no agent.\n\n## input\n\n```bash\ncotal input --name <n> --text <text> [--no-enter] [--on <instance>] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--name <n>` | | Managed agent to type into (required) |\n| `--text <text>` | | The text to type, taken verbatim (required) |\n| `--no-enter` | off | Type the text and stop there, without pressing Enter |\n| `--on <instance>` | class anycast | Pin to one manager instance id using the same rules as [`attach`](#managed-seats) |\n\nTypes one line into a running agent's terminal, as if you had typed it there, and returns. This is\nthe half of [`attach`](#managed-seats) that a program wants: `attach` is a live stream that holds a\nsession open and expects a terminal on your side, so a script, a cron job or a web UI cannot use it\nto send a single line. `input` is one authorized call.\n\nWhat it is for is **harness commands**. A line beginning with `/` is not chat and not a message: it\nis something the agent's own harness handles, and the only way in is the keyboard.\n\n```bash\ncotal input --name reviewer --text \"/compact\" # ask the harness to compact its context\ncotal input --name reviewer --text \"/model opus\" # switch its model\ncotal input --name reviewer --text \"hold on that PR\" # ordinary typing works too\n```\n\n**Quoting.** `--text` takes a value, so a payload starting with `/` survives as written. A payload\nstarting with a dash needs the `=` form, because the shell-style `--text --foo` is ambiguous and is\nrefused rather than guessed:\n\n```bash\ncotal input --name reviewer --text=--verbose # dash-leading text: use --text=<value>\n```\n\nEnter is pressed by default, since a command typed but never submitted has not been delivered.\n`--no-enter` types the text and leaves it sitting at the prompt, which is how you stage a line and\nsend it later.\n\nNothing comes back but a `Terminal input written` receipt. It names the agent, space and manager,\nthe bytes the manager reported writing (counting the trailing carriage return) and whether Enter was\nrequested. It confirms the write to the terminal only. Whatever the agent does next shows up where\nits output already goes: the mesh, its transcript, or an `attach`.\n\nA write the manager cannot confirm, such as a terminal write that failed partway, exits 3 with\n`Outcome unconfirmed` and is not typed again. Part of the text may already be at the prompt, so check\nthe seat before you send it again.\n\n**This one is operator-only, and more narrowly than `stop` or `attach`.** Those two are granted to\nanything holding `spawn`, so an agent can stop and attach to seats under its own owner. `input` is\nnot: it is granted only to operator credentials, which on a user-auth mesh means your ledger row\nneeds the `admin` scope, the same scope [`ps`](#managed-seats) already needs there. The reason is\nthat a write into a terminal is control of whatever is running in it, and on a user-auth mesh the\nown-owner rule covers every seat under you, not only the ones you launched: a `spawn`-scoped agent\ncould otherwise type into a sibling it never started. Seat locality is still resolved for you.\n\nOnly the `pty` runtime can be typed into. The external terminal runtimes (`tmux`, `cmux`, `orca`,\n`herdr`) attach to a process they do not own, so they have no input stream for it and the command\nrefuses by name rather than dropping the keystroke.\n\n## persona\n\n```bash\ncotal persona [--json|--plain]\ncotal persona show <name> [--json|--plain]\ncotal persona create <name> --file <persona.md>\ncotal persona create <name> --prompt <text> --subscribe <a,b>\ncotal persona edit <name>\ncotal persona remove <name> --yes\n```\n\n`p` aliases `persona`; `persona rm` aliases `persona remove`. These commands manage the\nselected space's local `.cotal/agents` catalog without connecting, refreshing a remote catalog\nor seeding connectors. `--space` and `--server` select the same root used by launch. A prepared\nspace has an offline catalog before it is started. Failed target resolution never falls back\nto the current directory. Remote `--on` operations refuse as unsupported here.\n\nCreation accepts one of `--file/-f` (a complete [agent file](agent-files.md)), `--prompt`, or\n`--from` (body file, or `-` for explicit stdin). Complete files validate all supported fields,\nmust name the requested persona and keep their original bytes. They cannot be combined with\n`--role`, `--model` or `--subscribe`. Body input requires an explicit `--subscribe` declaration,\nincluding `--subscribe \"\"` for no channels, and accepts `--role` and `--model`.\nCreation refuses an existing definition. Mutation receipts contain names and paths, not bodies.\n\nEditing opens a private draft using the editor preference (`VISUAL`, then `EDITOR`, then saved\npreferences). A successful editor exit and valid definition are required before saving. A changed\nor removed source refuses with `CONFLICT` (exit 2), leaving the current definition intact. Local\nCLI create, edit and remove share a per-persona lock during comparison and commit, not while an\neditor or confirmation is open. Manager and external writes do not participate in that lock.\n`--json` and `--no-input` refuse editing before an editor opens.\n\nList, show and mutation receipts support one-document `--json` and line-oriented `--plain`.\nShow explicitly returns the full definition and source. List does not return the body or launch\noptions. A malformed entry produces a partial inventory (exit 4); an unreadable catalog refuses\nrather than reporting it empty. Remove asks with cancellation as the default in a terminal.\nPlain confirmation uses numbered choices. Scripts and JSON removal require `--yes/-y`.\n\nThe legacy `personas` parser remains available during migration: `list [-v] [--running]`,\n`show`, `edit`, `new` and `rm --force`. Its `new` requires `--subscribe`, reads `--from -` from\nstdin, and overwrites an existing persona only with `--force`. Its local writes share the same\ndraft validation and commit fence. Only its explicit `--running` overlay connects to the mesh.\n\n## supervise\n\n```bash\ncotal supervise [--execution runtime|none] [--runtime <name>] [--space <s>] [--server <url>] [--spawn <names>]\n```\n\n`supervise` is a deprecated entry kept for existing units, scripts and containers, and help and\ncompletion omit it. It accepts the same flags and prints a warning on every run naming the\n`manager` service preset and the release that removes it; see\n[deprecated daemon entries](UPGRADING.md#deprecated-daemon-entries). The manager that `cotal up`\nstarts runs the same code through a hidden entry and prints no warning.\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space to supervise. On a root that hosts several spaces, name one of them here |\n| `--server <url>` | hosting mesh, or matching registered mesh | Broker URL. A registered mesh supplies it when omitted; a different explicit value is refused before anything is dialed. |\n| `--runtime <name>` | `pty` | Agent runtime (`pty` built in; extension runtimes are explicit-only) |\n| `--execution runtime|none` | `runtime` | NONE is a genuine hosting user-auth control instance with no Runtime, connector inspection, workload or terminal execution |\n| `--console-port <n>` | none | Protocol-console port |\n| `--console-host <host>` | loopback | Bind host for the console endpoint. Loopback keeps it machine-local; `cotal up` passes the address it bound the broker to, which is what lets the browser console reach this manager from another machine. `cotal attach` does not use this face: it redeems a mesh session grant over the broker |\n| `--max-sessions <n>` | 64 | Live-session ceiling. Each console pane and each `cotal attach` is one session, so size for agents \xD7 panes, not agent count. A capacity refusal names this flag. `cotal up --max-sessions` records the same number on the mesh so a later `supervise` started by repair keeps it |\n| `--roster <file>` | none | Declarative roster to boot at startup. See [Roster files](define-a-team.md#roster-files) |\n| `--launch <spec>` | none | Resolved manifest launch spec (from `up -f` / `team boot`) |\n| `--spawn <names>` | none | Comma-separated personas to pre-spawn at startup |\n\n`--ws-port`, `--resume-attempt` and `--resume-commit-token` are passed by `cotal up` when it starts\nthe manager, for the console session client and for resuming preserved seats. They are not for\ndirect use.\n\nThe manager is the agent supervisor and control plane: it answers `spawn --detach`, `stop`, `ps`,\n`attach`, and the `cotal_*` manager tools. `cotal up --detach` starts one for you; run `supervise`\ndirectly to recover a dead manager or drive a custom runtime. Default runtime is `pty`; install an\noptional provider first (`cotal ext add @cotal-ai/orca`, `@cotal-ai/tmux`, `@cotal-ai/cmux`, or `@cotal-ai/herdr`) and\nselect it explicitly. A missing provider or app fails loudly; there is no fallback. See [Deploy](deploy.md).\nBoot inventory decides whether this process takes unpinned `spawn`/`launch` on the class rail:\nif every declared connector is unavailable, those commands stay on this instance rail only\n(`status` reports `classSpawn: false`). `describe` still answers on the class rail, so an\nunpinned spawn can bind-fence against a skip member; re-issue, or pin `--on`. A partial\ninventory keeps the class rail and names `--on` on a harness refusal, because sibling\ninventories are not readable from the serve credential. See [control surface](control-surface.md#instance-routing).\n\nUse `--execution none` only for a fresh control instance in an existing hosting user-auth root.\nIt requires that space's signing trust, provider pin and user-mode mesh record, without remote\nmanager authority. It rejects `--runtime`, startup workload flags and participant registrations\nbefore resolving a Runtime or connector. The mode needs no connector seeding. An existing\nunclassified identity or retained workload refuses admission; a recorded NONE instance cannot\nrestart as runtime. Do not delete identity or capability files to change modes.\n\nNONE retains native registration, status and the selected Manager's empty managed-seat ps.\nIt does not adopt foreground agents, roster peers or HTTP-hosted sessions. Workload operations\nstill need an explicitly selected execution-owning participant Manager and its existing authority.\nStarting a control instance does not enable an auth service's interactive local-primary policy.\nNONE also refuses model inspection and caller-driven persona, purge, transcript, preservation,\nresume, cancellation, turn-yield and self-stop writes. It starts no transcript receiver or sweep.\nIts native registration, lease, renewal, session-ledger and goal maintenance can still write.\n\nLocal runtime startup now records identity and execution mode during admission. A later failed\nauthentication or registration can leave those files classified as runtime. Invalid supervise\narguments refuse before connector seeding, with one error line rather than seeding first.\n\nOn a normal `SIGINT`/`SIGTERM`, the manager stops every seat and requires the selected runtime to\nprove the seat is gone before it releases the manager lease or service registration. A stop that\ncannot prove exit fails loud and keeps manager authority instead of reporting a clean shutdown while\nan orphan still holds broker rails. When the runtime refuses the stop itself, the shutdown fails at\nonce and its error names the refusal as `stop failed: <message>`. After an abrupt manager death, the same logical successor\nterminalizes only its own durable static slots, verify-evicts the predecessor's broker principal,\nrecords that result in the lifecycle's caller-readable audit detail, reaps the predecessor's seat\nprocess through the runtime's custody reference recorded on the slot (the pty runtime verifies the\nprocess start identity in its seat record, so a reused pid is never signalled), and only then\nretires the lifecycle and frees the alias. A seat whose spawn carried `supervise` with restarts left\nis restarted instead of retired: the successor reaps any seat the slot still names, relaunches it\nunder the same name, lifecycle uid, credential, persona and cwd, and counts the restart against the\npolicy. A seat on a runtime that can adopt a live seat (tmux) and still running when the successor\nstarts is adopted in place: the successor checks that the recorded tmux server, window and pane are\nthe ones the slot names and that the pane's process still runs, then manages that seat again under\nits existing credential, without a restart. Both paths read the record the manager writes to\n`.cotal/seat-records/<space>/<name>.json` once a supervised or adoptable seat is ready, rewrites at\nevery restart and credential renewal, and removes when the seat is freed. A spent budget, a missing\nrecord, an expired credential, a persona file or credential that no longer matches the record, or\nan adoptable seat whose pane has exited retires the lifecycle as above. A runtime that custodies its seats reserves that\nreference before it launches one, and the manager records it on the slot's first durable row, so a\nmanager that dies part-way through a spawn also leaves a seat its successor can address. A\nsame-lifecycle restart or a resume records the new seat's reference on the slot the same way, and\nwhen the slot does not take it the restart or resume fails and stops any seat it started, so the\nslot never names a seat that has already exited while its replacement runs. A resumed seat keeps\nits retained credentials, so the resume frees it only once its exit is proved; a seat whose stop\ncannot be proved stays managed, and the resume's error says so, naming a stop the runtime refused\nas `stop failed: <message>`. A spawn\nthat launched its seat and then failed is rolled back by the manager that launched it, and that\nrollback reaps the seat through the same reserved reference before the lifecycle retires. Missing or unverified broker evidence keeps the slot\nterminalizing, and so does a runtime that cannot reap by reference. While the name stays held, a\nsame-name spawn or a resume re-drives that teardown with the same recorded reference, so it reaps\nthe seat again or keeps the name held.\n\nA `meshes add --mode user` entry is a **participant** registration, not hosting authority. A\nparticipant may run `supervise` only when the host advertises the remote manager authority service\nand the signed-in actor has the dedicated `supervise` ledger scope. The CLI obtains the closed,\nloopback-only `manager-service` view; `spawn` and `admin` do not substitute for that scope. The\nhost issues the manager's public-nkey JWT material through its lifecycle-bound prepare \u2192 activate\n\u2192 renew protocol, never by handing the participant a signer or static provisioner credential.\nThe host also performs instance-scoped eviction and guarded gate reconciliation. A remote manager\nrefreshes its short-lived registration executor before clean deregistration, so a long-running\nprocess removes its service row on `SIGINT` or `SIGTERM`. After an unclean stop, the same instance\nverify-evicts its superseded family and advances the process epoch. If an abandoned frozen gate\nholds the manager governance slot, a different supervise-scoped manager asks the host to reconcile\nthat holder after a complete gone verdict, then retries its registration once.\n\nA remote supervise never uses local signing trust: with host-issued authority in hand, the\nmanager mints from that authority alone and consults local records only to refuse a conflict,\nnamely the supervised space's own trust records under the cwd root. A root that hosts another\nstatic space beside the sign-in is a normal configuration and is never read as this space's\ntrust.\n\nThe broker URL in the registry entry decides the transport. A remote broker is often published\nover a `wss://` edge rather than a raw `nats://` port, and `supervise` dials whichever scheme the\nrecord holds, starting with the manager-authority registration it runs before the manager exists.\nThe record also decides whether that registration requires TLS, so a participant never downgrades\nthe credential exchange to a plaintext connection the registry did not describe.\n\nWithout that advertised host service or scope, `supervise` refuses before it starts a manager.\nRun `cotal spawn` without `--detach` to launch a foreground agent, or ask the space host to enable\nthe authority service and grant `supervise` for detached agents. If a running remote manager loses\nrenewal, it reports degraded state and refuses unsafe new starts and restarts; live agents are not\nsilently replaced. Do not run `cotal down` or `cotal up` on a participant machine to repair this\ncondition.\n\n## daemon\n\n```bash\ncotal daemon serve [--json|--plain]\ncotal daemon status [--json|--plain]\ncotal daemon login --idp <auth base URL> [--client-id <id>] [--json|--plain]\ncotal daemon connect-check --space <s> [--json|--plain]\ncotal daemon register --space <s> [--json|--plain]\ncotal daemon service-connect --space <s> [--json|--plain]\ncotal daemon service-disconnect --space <s> [--json|--plain]\ncotal daemon service-status --space <s> [--json|--plain]\n```\n\nThis release serves a local endpoint for status, sign-in, a connection check, a registration request\nand the daemon's own service connection. It does not start a Manager, serve any command on its\nservice, connect to Cloud, or install a system service. It supports Linux only. On other\nplatforms, an unset `XDG_RUNTIME_DIR` produces\n`TARGET_UNAVAILABLE` (`NO_RUNTIME_DIR`); with it set, the platform check refuses `UNSUPPORTED`.\n\n`serve` runs in the foreground and prints `daemon ready at <socket>` once the endpoint is bound\nand checked. That success line is plain text; `--json` shapes its refusals. `status` prints the\nstate, version, protocol, process ID, start time and uptime. `state: \"ready\"` means only that the\nlocal control listener is bound and verified, not that a mesh or Cloud connection exists.\n\nThe endpoint is `$XDG_RUNTIME_DIR/cotal/daemon.sock`. Without `XDG_RUNTIME_DIR` both forms refuse\n`TARGET_UNAVAILABLE` (`NO_RUNTIME_DIR`). Both refuse `PERMISSION_DENIED` for a runtime directory\nthat is a symlink, not a directory, not owned by you, or not mode `0700`. `status` also refuses\n`PERMISSION_DENIED` for an endpoint entry that is not a socket, not yours, or not mode `0600`.\nThe daemon drops connections from other users. The kernel separates users, not processes of one\nuser: any process running as you can use the local controls, including sign-in and cancellation.\n\n`status` with no daemon running refuses `TARGET_UNAVAILABLE` (`NOT_RUNNING`). `serve` refuses\n`CONFLICT` (`ALREADY_RUNNING`) for any existing socket, including one left after a crash, and\n`PERMISSION_DENIED` (`SOCKET_NOT_SOCKET`) for a non-socket entry. It removes neither.\nSIGTERM and SIGINT shut down cleanly and remove only the daemon's own socket. SIGKILL or SIGHUP,\nincluding closing the terminal, leaves a stale socket. To recover, run `cotal daemon status`.\nIf it reports `NOT_RUNNING`, confirm no daemon process is running, then remove the named socket\npath by hand and start again.\n\n`login` signs the running daemon in with device approval. The daemon runs the same sign-in as\n`cotal login` and keeps the session in the machine's Cotal home, where `cotal login` keeps it. The\ncommand prints only the approval link, code and expiry to stderr, then the signed-in account. No\nsession token crosses the endpoint or reaches the terminal. `--idp` takes an HTTPS auth URL, or an\nHTTP loopback IP URL; `--client-id` defaults to `cotal-cli`.\n\nOne sign-in runs at a time: a second `login` refuses `CONFLICT` (`LOGIN_IN_PROGRESS`) without\ncontacting the provider. Ctrl-C, or the command's connection closing, requests cancellation on this\nmachine. If cancellation takes effect before persistence, nothing is saved, even if device approval\nalready happened. An already-completed attempt remains completed; cancellation revokes nothing\nat the provider. A cancelled attempt reports `USAGE_INVALID` (`CANCELLED`). A denied or expired\napproval refuses `PERMISSION_DENIED` (`DENIED` or `EXPIRED`), and an unreachable provider refuses\n`DEPENDENCY_UNAVAILABLE`\n(`UNREACHABLE`); none of these saves a session. Any other provider answer fails `OPERATION_FAILED`\n(`REFUSED`). Such a failure can come after the session was saved, so run `login` again. A failure\nis reported with a fixed sentence; the provider's own error text is never relayed. `status`\nkeeps answering while a sign-in waits.\n\n`connect-check` asks the running daemon whether it could register with a space as its own service.\nThe daemon is not a Manager and never borrows a Manager's identity or authority. The check reads\nthe space's registration on this machine and the daemon's sign-in, then the space's public account\nidentity from its auth service (`GET <exchange>/service-endpoint-authority`, no redirects). It sends\nno registration and holds no credential. A space whose auth service does not host service endpoints\nrefuses `UNSUPPORTED` (`UNSUPPORTED_BY_SPACE`). The daemon does not fall back to anything else.\n\nThe other refusals are `TARGET_NOT_FOUND` (`NOT_REGISTERED`) for a space this machine has not\nregistered, `UNSUPPORTED` (`NOT_USER_AUTH`) for a space without user sign-in, `AUTH_REQUIRED`\n(`NOT_SIGNED_IN`) when the machine is not signed in to the space's provider, and\n`DEPENDENCY_UNAVAILABLE` (`UNREACHABLE` or `REFUSED`) when the auth service cannot be reached or\nanswers with something unusable.\n\nOn success it reports `ready-to-register` with the space's account, the daemon's service name\n`ai.cotal.machine` and the daemon's own instance ID. Nothing has been registered. The space owner must\nstill approve the daemon, and the service name needs the space operator's binding. The first\nsuccess creates the daemon's identity for that space at\n`<COTAL_HOME>/daemon/<space segment>/service-identity.json` (mode `0600`). It holds the instance ID\nand the account, and no credential. Later checks reuse it. If the space later answers with a\ndifferent account, the check refuses `CONFLICT` (`ACCOUNT_MISMATCH`) and changes nothing: confirm\nthe space was not replaced before you remove that file by hand.\n\n`register` asks the running daemon to request its own service registration with a space. It first\nruns `connect-check` and returns any outcome other than `ready-to-register` unchanged, without\ncontacting the space again. Otherwise it sends one registration request for the daemon's instance\nwith this machine's sign-in, through the space's stock service authority. The daemon never sees or\nrelays that sign-in proof. The request ID is saved before the first send, at\n`<COTAL_HOME>/daemon/<space segment>/register-request.json`, and every later `register`, including\nafter a restart, replays the same request unchanged. The stock writer does not guarantee durability\nacross power loss.\n\nThe fixed service name binds to one owner per space. Different owners in a shared space need distinct\noperator-approved endpoint names; this command does not yet select one. The operator must verify or\ndelegate the namespace before binding it.\n\nThe space answers in one of three ways, each reported with exit 0:\n\n- `approval-required`: the space owner must approve this daemon and the space operator must bind its\n service name `ai.cotal.machine`.\n- `pending`: the space is processing an approved registration.\n- `registered`: the registration is recorded. It is not activated or serving, so the machine is\n still not connected.\n\nNo release ships a command or page that lets an owner approve the daemon or an operator bind its\nname. A refusal by the space is `PERMISSION_DENIED` with the space's reason as the native code, such\nas `PROPOSAL_REFUSED`. A reason the daemon does not recognize is reported as `UNRECOGNIZED`, and the\nspace's own text is never shown. A new proposal after a refusal needs a new request, which `register`\ndoes not send. A sign-in the provider rejects (expired or revoked) is `AUTH_REQUIRED`: run\n`cotal daemon login` again. A failure before anything was sent is `DEPENDENCY_UNAVAILABLE`. Only a\nrequest whose outcome is unconfirmed is `OPERATION_UNKNOWN`: run the same command again\nto replay it. A changed registration for the same instance is refused `CONFLICT` and nothing is sent.\n\n`service-connect` connects the running daemon to the space as its own registered service. It replays\nthe saved register request unchanged to read its registration, gets its service credential from the\nspace's auth provider, verifies the bearer signature and witness against the pinned authority's\nissuer keys, checks the serving snapshot against its embedded contract, and connects to the broker\nas that service. It binds the registered\nresponder before reporting ready. It succeeds, exit 0, only after the broker accepts the connection,\nthe responder subscriptions are flushed and the space accepts the daemon as ready. The renewal and ready request IDs are kept in\n`<COTAL_HOME>/daemon/<space segment>/service-store/`, mode `0600`, and reused after a restart.\n\nEvery other answer is a refusal and nothing is connected. Not yet approved is `LIFECYCLE_BLOCKED`\n(`APPROVAL_PENDING`). A refused registration is `PERMISSION_DENIED`, and so is a space that ended the\ndaemon's authority, with its code as the native code, such as `APPROVAL_REVOKED`; connecting again\ndoes not reconnect it. A space or broker that cannot be reached is `DEPENDENCY_UNAVAILABLE`, and\nconnecting again retries the same requests. Without a register request the answer is\n`TARGET_NOT_FOUND`.\n\nWhile the daemon runs, the connection renews itself and reconnects on its own. The access bearer\nlives at most 60 seconds, so the broker drops the connection when each one expires and the daemon\nreconnects with the next. While the broker connection is down, for that reason or any other, the\nanswer is `connecting`, not connected; it is connected again only after the broker is back and the\nspace accepts the daemon as ready again. This is not a persistent service: a restarted daemon starts\nwith no service connection and stays disconnected until you run `service-connect` again, and an\nended connection is remembered only until the daemon stops.\n`service-status` reads the daemon's current answer without contacting the space. `service-disconnect`\ndrains the responder and closes the connection, and stopping the daemon closes all of them.\n\nThe service answers its existing `machine-status` command with `null`. This is a liveness response;\nit contains no vitals, hostname or machine metadata. Its stock Describe answer is explicitly public:\nit identifies the endpoint and derived owner token and includes the protocol and command schemas.\nCallers still need broker-enforced invoke access to the selected instance and command. Connecting the\ndaemon grants no caller access. Owner approval, operator name binding and caller grants remain\nseparate actions. An authority that does not publish its issuer identity cannot support this\nserving connection; the daemon refuses rather than trusting a decoded token or using Manager\ncredentials. The registration contract is unchanged.\n\n## service\n\n```bash\ncotal service [--space <s>] [--json|--plain]\ncotal service show <name>\ncotal service add <name> --preset <preset> <preset-options>\ncotal service add <name> --file <definition.json>\ncotal service start <name> [--instance <id>|--new-instance] [--foreground|--dry-run]\ncotal service stop <name> [--instance <id>] [--with-agents] [--yes|--dry-run]\ncotal service restart <name> [--instance <id>] [--with-agents] [--yes|--dry-run]\ncotal service status <name> [--instance <id>]\ncotal service logs <name> [--instance <id>] [--tail <n>] [--follow]\ncotal service install <name> [--instance <id>] [--dry-run]\ncotal service uninstall <name> [--instance <id>] [--yes|--dry-run]\ncotal service remove <name> [--yes]\n```\n\n`svc` and `services` are aliases. Every form takes `--space`, `--json` and `--plain`.\n\n| Flag | Meaning |\n|---|---|\n| `-s, --space <s>` | The space whose services to use, resolved as in [Selecting a space](#selecting-a-space). A space that is prepared but not started can be named |\n| `--preset <name>` | add: save a declared preset. Its options become flags of `add` |\n| `-f, --file <definition.json>` | add: import a version-1 definition file |\n| `--instance <full-id>` | Select one instance by its full ID, not a prefix |\n| `--new-instance` | start: allocate another instance of a process definition |\n| `--foreground` | start: keep the process in this terminal |\n| `--with-agents` | stop, restart: request the manager's native agent-stop policy |\n| `--dry-run` | start, stop, restart: preview the effect. install, uninstall: inspect the unit |\n| `-y, --yes` | stop, restart, remove, uninstall: confirm in a script |\n| `--tail <1..1000>` | logs: how many records to read (default 100) |\n| `--follow` | logs: keep reading |\n\nA service is a saved definition in one space plus the instances started from it. Bare `service`\nlists the definitions, their instances and the installed presets. Nothing starts a dependency for\nyou: starting the manager does not start its broker. `--json` writes one envelope for every form,\nand `logs --follow --json` writes one record per line.\n\n### Adding a service\n\nThe presets built into the CLI are `auth-service`, `broker`, `delivery`, `exec-endpoint`,\n`feedback-intake` and `manager`. The `linear` and `web` presets come with the `@cotal-ai/linear`\nand `@cotal-ai/web` extensions, which `cotal ext add <package>` installs. Bare `service` lists the\npresets this machine has, and a name it does not have is refused as `DEPENDENCY_UNAVAILABLE`.\n\n`cotal service add <name> --preset <preset> --help` lists a preset's options. A required option is\nmarked `(required)`, and an option that takes a path shows `<dir>` or `<file>`. A missing required\noption is refused with each one named.\n\n`add` checks the options against the preset's schema, and checks that each declared path exists\nwith its required protection. It does not check that a root or credential suits the preset, such\nas a root's auth mode or which credential a file holds. Those are checked when the service starts.\nA refusal names the option or field at fault. Option values are not printed.\n\n`--file` imports a version-1 definition. This one runs a program and treats it as ready once its\nloopback health URL answers 200:\n\n```json\n{\n \"schemaVersion\": 1,\n \"space\": \"main\",\n \"launch\": { \"type\": \"process\", \"executable\": \"/usr/bin/node\", \"args\": [\"/srv/reviews/server.mjs\"], \"cwd\": \"/srv/reviews\" },\n \"readiness\": { \"type\": \"http\", \"url\": \"http://127.0.0.1:8080/health\", \"expectedStatus\": 200, \"timeoutMs\": 10000 },\n \"envRefs\": { \"REVIEWS_TOKEN\": \"REVIEWS_TOKEN\" },\n \"credentialRef\": { \"kind\": \"file\", \"path\": \"/srv/reviews/reviews.creds\" }\n}\n```\n\n| Field | Meaning |\n|---|---|\n| `schemaVersion` | `1` |\n| `space` | The selected space's name. Another name is refused |\n| `launch` | `{type: \"process\", executable, args, cwd}`: an executable on `PATH` or by path, its arguments, and an absolute working directory. Or `{type: \"adapter\", adapter, options}` for an installed adapter, which a preset writes for you |\n| `readiness` | `{type: \"process\", timeoutMs}`: ready once the process runs. `{type: \"http\", url, expectedStatus, timeoutMs}`: ready when a loopback `http://127.0.0.1` or `http://[::1]` URL answers that status. An adapter definition uses `{type: \"adapter\", timeoutMs}`. `timeoutMs` is 1 to 120000 |\n| `envRefs` | Optional. Child variable name to the name of a variable in the environment `start` runs in. `CLAUDECODE` and names starting `COTAL_` or `CLAUDE_CODE_` are refused |\n| `credentialRef` | Optional. `{kind: \"file\", path}`: an absolute, protected credential file. The child receives its path as `COTAL_CREDS`, never its contents |\n\nNo other field is accepted. The file is at most 256 KiB.\n\n### Running a service\n\n`start` records the instance before it launches anything and prints its full ID. A start that fails\nbefore its process exists records the instance as failed. Its refusal names the cause, such as a\nspace that is prepared but not started. `start` can then try again on the same instance, and\n`stop`, `remove` and `cotal down` find nothing to stop.\n\n`stop` and `restart` ask before they act in a terminal and need `--yes` in a script. Enter, Ctrl-C,\nCtrl-D or closed input at that question leaves the service unchanged and exits 0. Without an\ninstalled unit, a `restart` whose process is already gone stops nothing, so it does not ask. A\nstart or stop that finds nothing to do says so and reports `no-op`.\n\n`remove` forgets a definition once every instance is stopped and no unit is installed. Logs stay.\n\n`install` writes an owned systemd user unit for a saved direct-process definition with no\n`envRefs` or `credentialRef`, and enables it without starting it. It is Linux only. `uninstall`\nremoves that unit. A preset is not installed as a unit in this release, the manager included.\n\nThe sections [Local service definitions](#local-service-definitions),\n[Service presets](#service-presets) and\n[Owned process services](#owned-process-services)\ndescribe each preset and the process driver in detail.\n\n## reconcile-gate\n\n```bash\ncotal manager reconcile [--space <s>] [--server <url>] [--endpoint <e>] [--instance <id>] [--yes|--dry-run] [--json|--plain]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space the frozen gate lives in |\n| `--server <url>` | the local mesh | Broker URL |\n| `--endpoint <e>` | `manager` | Endpoint whose gate is frozen |\n| `--instance <id>` | this folder's persisted manager instance | Instance id |\n| `--yes` | ask and default to cancellation | Confirm this selected gate repair outside a terminal |\n| `--dry-run` | off | Resolve the local coordinate without minting or checking native guards |\n| `--json`, `--plain` | terminal report | Finite structured receipt or plain shared report |\n\n**When you need this.** A manager restart killed after deregistration begins but before the new\nincarnation finishes leaves the endpoint's issuance gate *frozen*, held by a\nprocess that no longer exists. The freeze is what stops two incarnations serving at once, which is\ncorrect. The successor manager now completes that dead registration itself on boot, including on\nthe remote user-auth path. A foreign remote manager blocked by this gate also asks the host to repair\nit before one registration retry. Both use the same guard this command uses: they act only when the freeze-holder is affirmatively gone under a complete\nCONNZ sweep (`gone` and `sweepComplete=true`). If that registration's spec write already committed,\nit finishes the same freeze at the committed registration revision. If the spec did not advance, it\nabort-reopens the gate at generation+1 with processEpoch unchanged and continues the normal takeover.\nLive, unknown, unestablishable, and\nwrong-op-kind still refuse; there is no TTL.\n\nUse this command when the automatic path cannot run: the delivery daemon is down, the repair targets a\nnon-manager endpoint, or you want to lift the freeze without starting a manager. It checks that the\nholder really is gone, then finishes the dead operation the same way as the\ninterrupted restart would have: revoke the old credentials, evict their holders with verification,\nand reopen the gate.\n\nThe command revokes the old credentials 16 at a time. It then verifies the holders' eviction in\nshared sweeps of up to 256 holders on the delivery daemon. Each sweep scans the broker a fixed number\nof times and kicks live connections 16 at a time, so holders that are already gone add almost\nnothing and live ones add one broker round trip per 16 connections. The daemon must serve the\n`evictPrincipals` verb; an older daemon refuses it and the gate stays frozen.\n\nEach sweep durably records the holders it verified before the next sweep starts. If a holder is not\nverified gone, the command leaves the gate frozen with those records kept. An interrupted sweep\nrecords nothing, and the sweeps before it stay recorded. A retry still repeats the freeze-holder\nliveness check, then skips only progress bound to the same registration operation, frozen-gate\nrevision, and holder set.\nThe output reports holders completed before this attempt, completed now, and still remaining. A new\nfreeze or changed holder set starts from zero. Cursor cleanup happens only after reopen; a retained\ncursor is harmless because its old gate revision cannot authorize a later freeze. The governance slot\nrelease after the reopen is cleanup in the same way: a slot it fails to release does not fail the\nrepair, because the reopened gate is already past the slot's generation.\n\n**It refuses far more often than it acts, on purpose**, and always says which check stopped it:\n\n| Refusal | What it means | What to do |\n|---|---|---|\n| `holder-alive` | The freeze-holder still has a live connection: a manager *is* running | Stop that process first. Reconciling would evict a live manager's credentials |\n| `holder-unknown` | The connection sweep could not prove the holder absent | Not safe to proceed: an unprovable holder is treated as a live one. Re-run once the broker answers completely |\n| `liveness-unestablishable` | The delivery daemon gave no verdict: it was unreachable, timed out, or refused | Act on the delivery lease line in the refusal (below). Silence is never read as death |\n| `not-frozen` / `no-gate` | The gate is open, or there is no gate at that coordinate | Nothing to repair: check `--endpoint` / `--instance` |\n| `wrong-op-kind` | Frozen under a takeover or retirement, not a registration | Out of scope for this command; it will not reinterpret another operation's intent |\n| `eviction-unverified` | The holder looked gone but eviction could not be verified | The gate is left frozen, unchanged. Investigate the broker before retrying |\n| `raced` | A newer manager moved the gate mid-repair | Re-run `cotal doctor` and look again |\n\nWhen the daemon gives no verdict, the refusal also reads the delivery lease (`lease.0`) and names\nwhat is blocking the rail:\n\n| Lease reading | What to do |\n|---|---|\n| absent | No daemon is running. Start it (`cotal up` runs it) and re-run |\n| unreadable | The daemon cannot be named, so do not assume none is running. Fix the lease read, then re-run |\n| held, not ready | That holder claimed the shard and has not bound its rails. Wait for it, or stop it so its lease lapses |\n| held, ready, no answer | The query may have gone to another daemon still subscribed to the rail, such as a stopped one whose lease lapsed. Re-run before stopping anything. If no run gets an answer, stop any other delivery daemon for the space, then stop or restart the holder |\n| changed hands | The holder took the shard after the query was sent, so it was never asked. Re-run before stopping anything |\n\nThe command reads the lease before it sends the query and again after the query fails. It names a\nholder as the blocker only when the same run of the same daemon held the lease both times, and two\nrows from a daemon too old to record its run never count as the same run. Even then a ready holder\nmay not have been asked: the rail is queue-grouped, so any daemon still subscribed to it can take\nthe query. A row whose times are not valid dates reads as unreadable.\n\nA daemon that answered and refused keeps its own reason, followed by the same lease line. The lease\nline names the holder, whether it is ready, the space account that holds the lease bucket, when that\nholder acquired the shard, and when the row was last written. A ready holder rewrites the row on\nevery renewal and keeps its acquisition time, which only a successful acquisition sets. A row\nwritten by a daemon that predates the acquisition time reports it as unknown. The lease reads never\nchange the outcome: the gate stays frozen and the command exits 2. A manager's boot self-heal uses\nthe same check and reports the same line.\n\n`raced` means the repair's own reopen lost its compare-and-set and wrote nothing. A reopen whose\noutcome is unknown, such as one whose acknowledgement was lost, fails with an `unavailable` error\nthat says it may have committed. Re-run the command: it refuses `not-frozen` if that reopen landed and\nfinishes the repair if it did not.\n\nThere is no `--force`, and no path that discards gate state: the only way this reopens a gate is by\nproving the holder is gone and then completing the operation properly.\n\n**What reopening the gate does for the endpoint's governance slot.** A registration takes the\nendpoint-wide governance slot before it publishes its spec, and holds it until its gate reopens. An\ninstance that died between those two points leaves the slot held with no registration behind it.\nThis command does not write that slot and never has; the registration path is its only writer. What\nthe reopen does is advance the holder's gate past the generation the slot is stamped with, which is\nwhat marks the slot abandoned. The next registration for that endpoint then reclaims it as part of\nits ordinary start. So the repair here is still one command followed by starting the manager, and\nthe slot needs no separate step.\n\n## endpoint deregister\n\n```bash\ncotal endpoint deregister <class> <full-instance-id> [--space <space>] [--server <url>] [--yes|--dry-run] [--json|--plain]\n```\n\nThis removes a departed instance's registration, not a process or an agent. Both the native\nclass and full instance ID are required; there is no default instance, prefix match or saved\nreference. The selected static space must have validated local signing authority. Open and\nuser-auth targets refuse instead of borrowing a static credential path.\n\nThe command resolves one root, account and broker policy, including its TLS requirement.\nA terminal reviews that full target and defaults to Cancel. Scripts need `--yes`.\n`--dry-run` reports the offline target and intended guard without contacting the broker,\nissuing an operation credential or deleting anything. It does not prove apply will be allowed.\n\nApply delegates to the existing native guard. An answering instance refuses. An unanswered\ninstance also refuses unless the broker affirmatively reports that instance rail empty.\nAn unavailable probe or an in-flight registration refuses. After that evidence, the native\nhelper checks governance and deletes status before spec, pinning both revisions. The issuance\ngate is not changed and no process is stopped; the same instance may register on a later start.\n\nThe JSON/plain receipt retains the full coordinate and acknowledged removed revisions.\nA registration that moves during deletion is not success: its spec was not removed, but an\nolder status may already have been removed. That uncertain outcome retains the target and is\nnot retried. Raw probe diagnostics and credential material are not copied into the receipt.\n\nThe old `deregister-instance` spelling is a hidden migration refusal. It starts no operation\nand names the literal replacement; its former default-to-this-folder instance is not retained.\n\n## runtimes\n\n```bash\ncotal runtimes [--json | --plain]\n```\n\nLists every agent runtime the manager can spawn through: the built-in `pty`, the official providers\n(`orca`, `tmux`, `cmux`, `herdr`), and any custom provider installed via `cotal ext add`. Each installed\nprovider is probed so you can see what is actually reachable on this machine before selecting it:\n\n```\nRUNTIME STATE PACKAGE\npty built in\norca installed \xB7 reachable @cotal-ai/orca\ntmux not installed @cotal-ai/tmux\ncmux not installed @cotal-ai/cmux\nherdr not installed @cotal-ai/herdr\nadd one that is not installed with `cotal ext add <package>`\n```\n\n`installed \xB7 reachable` / `unreachable` is the provider's own `available()` probe; `not installed`\nmeans it is a known runtime you can add with `cotal ext add` and the package shown. `--json` writes\none document whose `result.items` are `{name, state, package, reachable, error}`, with `state` one of\n`built-in`, `installed` or `not-installed` and `reachable` null when the provider was not asked. An installed provider\nwhose package fails to load is shown with its error and makes the read `partial` (exit 4). The command\nreads this machine only, so its `target` is null. Selecting an unknown or uninstalled runtime\nvia `up`/`spawn --runtime <name>` fails loud and, for a known one, points at the exact `cotal ext add`\npackage. There is no silent fallback to `pty`.\n\n## seats\n\n```bash\ncotal seats [--drain]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--drain` | off | Retire every seat whose agent has exited. A seat whose agent still runs is kept |\n\nThe pty runtime used to start a detached custodian process for every Linux seat. It now spawns\nin-process, but custodians that an earlier manager started keep running, and one whose agent has\nexited stays resident while a manager still holds its connection. This command lists the custody\nrecords under `COTAL_SEAT_ROOT` (default `~/.cotal/seats`), one line per seat:\n\n| State | Meaning |\n|---|---|\n| `live-child` | The agent process still runs. The seat is never signalled, and a manager can still adopt it |\n| `childless` | The agent has exited, or the record comes from an earlier boot. `--drain` retires the seat |\n| `drained` | `--drain` proved the custodian and the agent gone and removed the record |\n| `refused` | The record cannot be read, carries no start or boot identity, this host publishes no boot identity, or the reap could not prove the processes gone. The record stays on disk |\n\nA drain signals only a custodian whose recorded start identity still matches the live process, so\na reused pid is never touched. No process outlives a reboot, so a record from an earlier boot is\nreported childless and `--drain` removes it without signalling anything. A record with no start or\nboot identity is refused with or without `--drain`, and is never reported as running or exited.\nOn a host that publishes no boot identity (`/proc/sys/kernel/random/boot_id`) every record is\nrefused the same way, because no record can be tied to this boot.\nThat refusal and an unreadable record signal nothing. A refusal from the reap itself can come after the drain already\nsent `SIGKILL` to the custodian. Its detail names the pid or process group the reap could not prove\ngone, so check those processes before you retry. The command exits non-zero when any record is\nrefused. It is Linux-only and throws on other platforms.\n\n## send\n\n```bash\ncotal send -t <agent> \"<text>\" [--space <s>] [--server <url>] [--creds <path>]\ncotal send -c <channel> \"<text>\"\ncotal send -r <role> \"<text>\"\n```\n\nChoose one destination: `--to/-t` for a direct message, `--channel/-c` for a channel,\nor `--role/-r` for the role's native work queue. Choose one body source: a quoted positional\nargument, `--file/-f <path>`, or explicit `--stdin`. There are no implicit stdin reads. Files and\nstdin must contain nonempty UTF-8 text; their content is preserved and never printed in the receipt.\nNo destination, several destinations, several body sources or an empty message is refused.\nThe recipient grammar is checked before connector installation or target preparation. Channel\nand role names use the native subject grammar; a channel publication cannot name a wildcard.\nA direct target resolves by its full identity or an unambiguous name. A name collision refuses;\nno fuzzy match selects an agent for you.\n\nFor a channel post, repeat `--mention <agent>` to add explicit wake hints without changing the\ndestination. Direct and role sends have no native mention field and refuse that option.\nA direct send may use `--reply-to <message-id>` to identify the message it answers. The sender\nstill has no inbox and does not promise thread continuity.\n\nThe receipt names the destination and message identifier without repeating the body. Direct and\nrole sends include the broker's stored sequence. Storage does not confirm delivery, acceptance\nor completion. A direct send to an agent the roster shows offline also warns on stderr:\n`<agent> is offline. The message is stored and delivered when <agent> reconnects; a new agent spawned as <agent> does not receive it.`\n`--json` lists it as the `RECIPIENT_OFFLINE` warning. Use `--json/-j` for the same receipt as one versioned envelope, `--plain` for a text report, or\n`--verbose/-v` to show the actual sending principal. An unregistered channel is labeled; its native\nspace policy still controls publication and no registry metadata is created. Unreadable metadata\nstays not checked. An unconfirmed publish outcome is unknown and never retried automatically.\nThe command sends once and exits. It does not wait for an answer, retain an inbox\nor transfer replies to a later chat session.\n\nThe sending principal comes from the selected space's credentials or user login. Its display name\nis the OS login and host, not `COTAL_NAME`. A one-shot sender never registers as a participant.\nThe broker and native channel policy still decide which publications are authorized.\n\nThe transient endpoint never joins the roster and binds no inbox. With a static or open-mesh\ncredential it reads the roster and the channel registry once when it connects and binds no watch\nconsumer, so it leaves nothing on the broker when it exits. On a user-auth mesh it runs on your user\nbearer, which has no batched read of those buckets, so it watches them instead, and each watch's\nconsumer stays on the broker for five minutes after the send exits. A sustained send rate there can\nreach the broker's limit of 1000 consumers per stream, and the broker refuses further sends until\nthe old consumers expire. A channel send also reads that channel's policy, with one Direct Get on a\nstatic or open mesh and with one more scan consumer on a user-auth mesh.\n\nA recipient can still answer a\n`send --to` or `send --role` with `cotal_dm`, by the sender's name or by the id on the message it\nholds: the reply is stored under the sender's id in the space's DM history, which an operator's DM\nview such as the dashboard's Direct messages lens shows. The `cotal send` that asked has already\nexited, so the reply never reaches that shell.\n\n## chat\n\n```bash\ncotal chat [<channel>] [--name <n>] [--plain | --stdio] [--space <s>] [--server <url>]\n```\n\n`chat` opens one live session in the selected space, under an identity of its own. Each opening is\na new principal with its own inbox and presence, so two windows are two sessions, and leaving ends\nthe session. `--name` sets its display label; the default is your OS login and host with a short\nsuffix. The label carries no authority.\n\nThe mesh is resolved once, and its recorded auth mode picks the admission. No other mode is tried.\n\n| Mode | Admission | Lifetime |\n|---|---|---|\n| open | Routing identity only, with no credential | Until you leave |\n| static | The space's manager admits the session and mints its own credential ([Static chat sessions](control-surface.md#static-chat-sessions)) | 15 minutes, never renewed |\n| user | The auth service admits it under your own `cli` grant; sign in with `cotal login` first | Up to 8 hours, never extended |\n\nA named channel is the session's initial channel. On a static or user-auth mesh it is also the\nsession's whole read and post list, so a session opened without one can send and receive direct\nmessages only. A send, join or channel mode outside that list refuses with `PERMISSION_DENIED`\nbefore anything reaches the broker. On a session opened without a channel, the refusal says to\nleave and open a new session on the channel. Before the session is admitted, `chat` reads the channel registry on the resolved\nmesh the way `send` does, under the same one-shot identity, and says on stderr whether the channel\nis registered, unregistered, or could not be read. The space's native policy decides whether a send\nis allowed, and nothing creates registry metadata.\n\nThe terminal view needs a terminal for input and output; without one, `chat` refuses and names\n`--plain` and `--stdio`. `--plain` is the line flow, with a separate confirmation for each send.\n`--stdio` runs the NDJSON participant protocol for programs: requests on stdin, records on stdout,\ndiagnostics on stderr. Closing stdin ends the session. `--stdio` does not combine with `--plain`,\nand `chat` has no `--json`. Under `--no-input` only `--stdio` runs.\n\nA direct message to an agent the roster shows offline is stored, and the view adds a notice that\nthe agent receives it when it reconnects, as [`send`](#send) warns. `--stdio` carries that roster\nstatus as `recipientStatusAtSend` in the stored result.\n\nMessage times show in local time, with the date when it is not today, the same way `history`\nshows them.\n\n`/reply` answers the latest direct message or role task the session received. It addresses the\nsender's principal and quotes the message id, with no name lookup. A sender shows by display name,\nand with its principal when another principal in the session carries the same name, ignoring case.\n`--plain` cannot relabel a line it already printed, so it prints one line naming those principals\nwhen a name becomes shared. A typed `@<name>` must name one principal among the present peers and\nthe senders the session heard from. A name that more than one of them carries refuses and lists\nthem, so type `@<owner>.<actor>` instead. A target that parses as `<owner>.<actor>` goes to that\nprincipal whether or not it is present or was heard from, and is never matched against display\nnames. A peer whose display name parses as a principal therefore cannot be reached by that name;\n`/reply` and its full principal still reach it. Typing the session's own principal refuses with\n`USAGE_INVALID`. A `--stdio` `dm` request resolves its `to` the same way.\n\nEvery admission presents the session as an endpoint, so `--kind endpoint` is accepted and changes\nnothing. `--kind agent` refuses with exit 1 because a chat session never presents as an agent, and\nso does any other kind.\n\n`--role`, `--link`, `--token`, `--creds` and `--lifecycle-uid` are declared but no admission\nsupports them yet, so each refuses with exit 2. A role would put the session on that role's work\nqueue, and the view has no way yet to answer the sender of a task. The static and user-auth\nadmissions take no role.\n\nA session that ends by leaving, by end of input or at its lifetime exits 0 when its connection\nclose was confirmed and it was retired, or was an open-mesh session with nothing to retire. It exits\n4 when its connection close was confirmed and its retirement was requested but not confirmed. It\nexits 3 when the retirement outcome is unknown, its connection close was not confirmed, or no close\nwas observed. In the terminal view Ctrl-C and Ctrl-D leave. With `--plain` or `--stdio`, Ctrl-C ends\ninput and the session closes, then exits 130. A terminate signal exits 143 and a hangup 129. A\nsignal that arrives before the view starts is held: no session is opened if admission has not\nbegun, and a session admitted meanwhile is closed at once. The exit status is still the signal's.\nWhen the reader of `--plain` or `--stdio` output closes it, as `| head` does, input ends the same\nway and the session closes, then exits with its close status: 0, 3 or 4.\n\nBefore a session is admitted, an error prints on stderr only, since `chat` has no `--json`. An\nerror that ends `chat` exits 1 for `USAGE_INVALID` or `SCHEMA_INVALID`, 2 for any other refusal, 3\nwhen the outcome is unknown and 5 when it failed.\n\n### The --stdio protocol\n\nstdin carries requests and stdout carries records, one JSON object per line, in UTF-8 with LF line\nends. A CR before the LF is dropped, a blank line is ignored, and a last line with no LF still\ncounts when input ends. A line longer than 1048576 UTF-16 code units is discarded up to its next LF\nand reported once with an `error` record. Input is decoded as UTF-8 with replacement, so bytes\nthat are not valid UTF-8 become U+FFFD. In ordinary string content the line can still parse: a\n`text` value is sent with U+FFFD in their place, and a field that accepts only some characters,\nsuch as `id`, refuses. In an escape sequence such as `\\` or `\\u` and its four digits, or outside a\nstring, they make that line's JSON parse refuse. stdout carries records and\nnothing else. The session's diagnostics go to stderr, prefixed `cotal chat: `, and never repeat a\nmessage body.\n\nEvery request and record carries `\"schemaVersion\":1`. A request names its `type` and an `id` you\nchoose, 1 to 128 printable ASCII characters, and its `result` record repeats that id. A request that\nreuses the id of one still in flight is refused with `USAGE_INVALID`, in a result under that same\nid. Requests run one at a time in arrival order, so results come back in that order. `message`,\n`presence` and `gap` records can arrive between results, so match each result by its id. A missing\nfield, a field the type does not take or a malformed value refuses with `USAGE_INVALID` before the\nsession sees the request. A channel must be a valid channel name with no `*` or `>`, and a role a\nvalid role name.\n\n| `type` | Fields | Result |\n|---|---|---|\n| `send` | `channel`, `text`, optional `mentions` (names of 1 to 128 characters) | `stored`: `messageId`, `channel`, `registered` |\n| `dm` | `to` (a display name or `<owner>.<actor>`, 1 to 512 characters), `text`, optional `replyTo` (a message id, 1 to 512 characters) | `stored`: `messageId`, `seq`, `duplicate`, `recipient` (`id`, `name`) |\n| `anycast` | `role`, `text` | `stored`: `messageId`, `seq`, `duplicate` |\n| `status` | `status` (`idle`, `working` or `waiting`), optional `activity` | `ok` |\n| `attention` | `mode` (`open`, `dnd` or `focus`) | `ok` |\n| `channelMode` | `channel`, `mode` (`normal`, `quiet` or `muted`) | `ok` |\n| `join` | `channel` | `ok`: `joined`, `backfilled`, `durable`, optional `reason` |\n| `leave` | `channel` | `ok`: `left` |\n| `inbox` | optional `peek` (default `false`) | `ok`: `peek`, `messages` |\n| `roster` | none | `ok`: `rows`, `view` |\n| `channelInfo` | `channel` | `ok`: `channel`, `registered`, `replay`, `readable`, `postable`, optional `description` and `instructions` |\n| `connection` | none | `ok`: `state`, `identity`, `bufferedInbox`, `joinedChannels`, `attention`, `channelModes` |\n| `invoke` | `endpoint` (1 to 200 characters), `command` (1 to 128), optional `args` (an object) and `timeoutMs` (an integer from 1 to 120000, default 30000) | `ok`: the endpoint's data, or `null` when it returned none |\n\n`identity` in the `connection` result names the session, and no other record names its space. It\nhas `space`, `mode` (`open`, `static` or `user`), `principal` (`<owner>.<actor>`), `lifecycleUid`,\n`label`, the `allowSubscribe` and `allowPublish` ceilings, and `expiresAt` in epoch milliseconds\nwhen the session has a set end. A roster's `view` is `current`, `unpopulated` or `stale`, and its\n`rows` are complete only when it is `current`. A row has `id`, `name`, `kind` and `status`, and\n`role`, `activity` and `sessionKind` when they are set.\n\nA `result` record has `type`, `id` and `outcome`. With `ok` or `stored` it carries `result`, which\nis `null` when the request returns nothing. With `refused`, `unknown` or `failed` it carries `error`\nwith `code`, `message` and, when the native layer gave one, `cause` with its own `code` and\n`detail`. `refused` means nothing happened. `unknown` (`OPERATION_UNKNOWN`) means the effect may\nhave happened. The session never retries it, and it reports an unexpected failure of its own the\nsame way. `failed` (`OPERATION_FAILED`) is a step that did not complete and had no effect. `stored`\nmeans the broker's stream accepted the message. It does not say anyone read it.\n\nThe session also writes these records on its own:\n\n| `type` | Fields | When |\n|---|---|---|\n| `message` | `message`: `id`, `ts` (epoch milliseconds), `kind` (`channel`, `dm` or `anycast`), `from` (`id`, `name`, optional `role`), `channel` or `service`, `text`, optional `replyTo`, `mentionsMe`, `historical`, optional `deliveryCount` | A message arrived |\n| `presence` | `change` (`joined`, `updated` or `left`), `row` (a roster row) | A roster row changed, this session's own row included |\n| `gap` | `scope` (`live` or `inbox`), `reason` | Messages may have been missed |\n| `error` | `id` (`null`), `error` | A line was refused before an id could be read from it, or was too long |\n| `end` | `reason`, `close` | The session ended. Nothing follows it |\n\nA message's `text` is untrusted. JSON escapes its control characters, so a body can never start\nanother record. `channel` is set on a channel message and `service` on an anycast. `mentionsMe`\nmeans the message's mention list holds this session's label, ignoring case. The sender writes that\nlist, so treat it as a hint. `deliveryCount` appears only on a durable delivery and is 1 the first\ntime. Above 1, an earlier copy went unacknowledged and may already have been shown.\n\nA message is acknowledged once its record is handed to stdout. A DM or anycast is written when it\narrives. A channel message is held in the inbox instead when its channel is quiet, when attention is\n`focus`, or under `dnd` unless it mentions the session, and join backfill is held too. A muted\nchannel's messages are dropped. `inbox` with `peek` lists the held messages and leaves them held.\nWithout `peek` it returns them, and they are acknowledged once its result is handed to stdout. A\nmessage still unacknowledged when the session closes is left unacknowledged and does not come back\nto this session.\n\nA `live` gap means the connection dropped or is reconnecting, so live channel messages may be\nmissed until it returns. Its reason defaults to\n`connection <state>: live channel messages may be missed until it returns`. An `inbox` gap means\nthe held buffer was full: channel messages were dropped and direct messages were left for the broker\nto redeliver. When a write waits for stdout to drain, or more than 4 MiB of records are waiting, the\nsession stops reading stdin until stdout drains.\n\n`end` is the last record. Its `reason` is `input closed` when stdin ended or a signal ended input,\nand `session ended` when the session ended by itself, for example at a static session's expiry.\nWhen stdin ends, the requests already read finish first. `close` has `retirement` (`retired`,\n`requested`, `unknown` or `not-applicable`), `connectionClosed` (`confirmed` or `not-confirmed`) and\n`message`. A static session's `close` can carry `credentialValidUntil` in epoch milliseconds: the\nbroker accepts its credential until then unless its eviction was verified. Once the session has\nended by itself and before the `end` record, a request refuses with `LIFECYCLE_BLOCKED` and\n`the session is ending; no new requests are accepted`. When the reader closes stdout, stderr says\n`stdout closed by the reader; ending the session`, and no later record is written, `end` included.\n\n## channel\n\n```bash\ncotal channel\ncotal channel set <name> [--replay | --no-replay] [--window <duration>] [--delivery <live|durable>] [--desc <text>] [--instructions <text>]\ncotal channel defaults [--replay | --no-replay] [--window <duration>] [--delivery <live|durable>]\ncotal channel show <name>\ncotal channel members <name> [--follow] [--json | --plain]\ncotal channel remove <name> [--purge] [--yes]\n```\n\n`channel` has the shorthand `ch`; `remove` also accepts `rm`. The inventory shows configured\nmetadata, not every channel with traffic. `show` includes inherited replay and delivery settings.\n`members` reads the broker-derived membership feed. Its timestamp is the last successful feed\npoll, not proof of current activity or access. With `--follow --json`, it emits membership NDJSON\nrecords. A missing feed remains unknown rather than an empty confirmed census.\n\nRemoval keeps retained history unless `--purge` is present. The confirmation names the channel,\nspace and history scope. Scripts pass `--yes`. Neither removal nor replay settings revoke grants\nor stop participants; future posts can create retained history again.\n\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Target mesh |\n| `--replay` / `--no-replay` | none | `set`/`defaults`: replay history to new joiners, or not |\n| `--window <duration>` | none | `set`/`defaults`: replay window, for example `24h` |\n| `--delivery <live|durable>` | none | `set`/`defaults`: choose the live-only or durable-backstop delivery class; history retention is separate. Shell completion offers both values |\n| `--desc <text>` | none | `set`: channel description |\n| `--instructions <s>` | none | `set`: instructions shown to joiners |\n\nInspects and edits the channel registry: replay and delivery policy, description, and joiner instructions. `live` has no per-member backstop; it does not disable ACL-bounded retained history. Changing a channel's delivery class does not settle already pending durable copies. `--delivery durable` requires a registered static-auth or user-auth space; an open or unregistered target has no established delivery-service support. A recorded auth mode does not prove that daemon is currently healthy. ACL\nsemantics (who may read or post) are set at mint / provision time, not here; see\n[Channels and permissions](channels-and-permissions.md). On a user-auth mesh, `list` rides your\nown login as is; `set` and `defaults` edit the registry over a short-lived\nchannel-writer view, which needs ledger scope `admin` ([Identity & auth](identity-and-auth.md)).\nOn a remote user-auth mesh that view is served by the public exchange; space-history `purger`\nand the read-only admin view are not.\n\nWith a static or open-mesh credential, `list` reads the registry with one Direct Get and leaves no\nconsumer on the broker. On a user-auth mesh your login has no Direct Get row on the registry, so\n`list` scans it with a consumer that stays on the broker for five minutes after the command exits.\n\n\n## history\n\n```bash\ncotal history [<channel>] [--dms] [--since <time|seq>] [--limit <n>] [--from <name>] [--grep <text>] [--follow] [--json | --plain]\ncotal history clear [--dms] [--yes | --dry-run] [--json | --plain] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Target mesh |\n| `--dms` | off | Also read DM history, which needs a DM read grant. With `clear`, also clear DM history |\n| `--since <time\\|seq>` | none | Only messages after an ISO 8601 time, an age such as `30m`, `2h` or `1d`, or a chat stream sequence |\n| `--limit <n>` | 50 | How many of the newest retained messages to read, 1 to 1000 |\n| `--from <name>` | none | Keep messages from this sender name or id |\n| `--grep <text>` | none | Keep messages whose text contains this, ignoring case |\n| `--follow` | off | Keep listening and print each new message |\n| `--json` / `-j` | off | One JSON document, or with `--follow` one JSON record per line |\n| `--plain` | off | The same report without color or marks |\n| `--yes` / `-y` | off | `clear`: confirm without prompting. Scripts and `--json` require it |\n| `--dry-run` | off | `clear`: show the selected target and streams without connecting, and purge nothing |\n\nReads the newest retained messages in the selected space, newest last: one channel, or every\nchannel the identity may read. `--limit` bounds the read, and `--since`, `--from` and `--grep` then\nfilter that window on this machine. They do not search the whole stream. `result.window` says how\nmany messages were read, whether older retained messages exist that the window did not reach, and\nwhether the window goes back to `--since`. An age outside the supported date range or a sequence\nthat cannot be represented without rounding is a `USAGE_INVALID` refusal before connecting.\nThe report shows each message's local time, with its date when it is not today; `result.items[].ts`\nis the ISO time.\n\nRetained history is not every message sent. The chat stream keeps a fixed number of messages per\nsender on each channel, so a busy sender's oldest messages are discarded while other senders'\nmessages from the same time remain. `result.retention` names the limit and each sender at it on the\nchannels read, and the report prints a `gap` line for each. A gap is a fact about the stream, not a\npartial read. `result.replay` gives each channel's replay policy for new members, read from the\nchannel registry. DM gaps are not reported.\n\nA channel the identity may not read is refused with `PERMISSION_DENIED` (exit 2), never shown as\nempty. On a user-auth mesh each channel read uses the connection's broker-enforced channel grant.\nRevocation follows the effective credential and connection retirement, as it does for live channel\nreads. Concurrent commands sharing one identity wait for the broker to release its transient history\nreader; no command deletes another reader. Each channel has a ten-second read budget, with a\nthirty-second budget for the whole channel walk. A busy or cut-short read fails with `OPERATION_FAILED` (exit 5) rather than showing\nan empty conversation. Reading every channel leaves out channels the grant does not cover and names\nthem in a `CHANNELS_DENIED` warning. DM history and `--since` by sequence are not available to a\nuser-auth identity. A part that could not be read is a warning and a `partial` run (exit 4): the\nretention, a replay policy, the channel list when a channel is named, or one channel of several.\n\n`--follow` prints the window and then each new message until interrupted. With `--json` each line is\none record: `{\"schemaVersion\":1,\"stream\":\"history\",\"type\":<type>,\"target\":\u2026,\"receivedAt\":\u2026,\"data\":\u2026}`.\nThe first record is a `snapshot` holding the window, then one `event` per message. A `gap` record\nsays the connection was re-established and messages sent while it was down are not shown. An\n`error` record names a failure on the live feed, and an `end` record closes the stream. The exit\ncode is the snapshot's.\n\n`history clear` purges retained history from every channel in the selected space; `--dms` extends\nit to direct-message history. It refuses extra channel names, read filters and `--follow` before\npurging. These inputs cannot narrow a clear operation. It is the same operation as [`clean history`](#clean). On a user-auth mesh the purge rides a short-lived\npurger view over your login, which needs ledger scope `admin` ([Identity & auth](identity-and-auth.md)).\n\n## console\n\n```bash\ncotal console [--plain] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Space to watch |\n| `--plain` | off | Line stream instead of the TUI |\n\nA live protocol view for a space: a lazygit-style TUI, or a plain line stream on `--plain`. On a\nuser-auth mesh it rides the read-only admin view over your login, which needs ledger scope\n`admin`. Inside the TUI, operator control (`D` kill, `:spawn`, `:status`, `:purge`) rides the\nsame per-action instrument path as `cotal stop` and `cotal ps`, never the observer; a raw\n`--creds` file cannot drive it. `a` (or `:attach <agent>`) runs\n[`cotal attach`](#managed-seats) in place and returns to the console on detach. See\n[Watch a mesh](watch-a-mesh.md).\n\n<a id=\"web\"></a>\n\n## Web dashboard\n\nThe dashboard is an owned service preset from `@cotal-ai/web`, not a top-level `web` command.\nIt is not started by `up`. An absent extension requires the explicit `cotal ext add @cotal-ai/web`;\nstarting a service never installs it or creates a caller credential.\n\n```bash\ncotal service add web --preset web --space main --root /absolute/registered/root --creds /absolute/private/dashboard.creds --read-only\ncotal service start web --space main\ncotal service status web --space main\ncotal service logs web --space main\ncotal service stop web --space main --yes\n```\n\nThis route requires a registered static-auth target, an existing protected credential file\nand explicit read-only intent. `--host` chooses a concrete bind/browser address, default\n`127.0.0.1`; wildcard binds refuse. `--port` is 1 to 65535, default 7799. Broker coordinates and\nrequired TLS come from the registered target. Broker TLS does not encrypt the dashboard's HTTP\nlistener. The file supplies the only connection authority.\nNo cached human login, managed-seat identity, operator signer or channel-purger credential is\nsubstituted. User-auth/open targets refuse, because no supported caller handoff is connected for them.\nThe page marks this service read-only and hides channel delete; the server also refuses it before\nreading a delete body or opening a mutation connection. The broker still enforces the file's scope.\n\nThe real child exclusively claims native PID custody. After the HTTP listener and signal handlers\nexist, it writes a create-only owner-private session record for the full instance and launch, then\na private process-initialization phase. This is not a fresh authenticated endpoint-health reply.\nLogs print only the session file's reference, never its bootstrap URL or readiness credential. A\nbackground `service start` ends with the `service logs` command for the new instance, which is where\nthat reference appears. An endpoint error or warning in that log names only the error's class, its\nname and code, never its message. Open the private file locally and use its single-use launch link in the\nintended browser. There is no automatic browser action. The existing cookie and origin gates remain in use. Confirmed close\nremoves only this child's unchanged session/PID records; uncertainty retains custody.\n\nForeground service start uses the shared terminal lifetime. OS installation and full native caller\nparity are not supported. An old cached `command:web` descriptor is hidden and refuses with the matching\nextension-update remedy before package loading or repair. It is not resolved by removing the\ndashboard, starting a second process or silently reinstalling it.\n\n## linear\n\n```bash\ncotal ext add @cotal-ai/linear\ncotal linear account add <name> --mode <write|readonly> (--token-stdin | --token-file <path>)\ncotal linear account login <name> --mode <write|readonly>\ncotal linear account show <name>\ncotal linear inventory <account> [--json|--plain]\ncotal linear call <account> <tool> [--args '<json>'] [--inventory <digest>] [--timeout <ms>]\ncotal linear resource <account> <uri> [--inventory <digest>] [--timeout <ms>]\ncotal linear prompt <account> <name> [--args '<json>'] [--inventory <digest>] [--timeout <ms>]\ncotal linear serve <account> --endpoint <reverse-dns-name> [--space <s>] [--server <url>]\ncotal linear caller <name> --endpoint <reverse-dns-name> --out <path> [--channels <a,b>] [--expires-in <s>] [--json|--plain]\n```\n\nServes one Linear account as a registered Cotal endpoint that agents call with `cotal_describe` and\n`cotal_invoke`, and gives the operator a direct client for the same server. It only talks to `https://mcp.linear.app/mcp`\n(mode `write`) or `https://mcp.linear.app/mcp/readonly` (mode `readonly`); there is no URL or header\noption. Use a separate account per Linear workspace, and prefer a `readonly` account with a\nrestricted read key wherever writes are not needed.\n\nAn account's credential is sent as `Authorization: Bearer` and is never taken from argv. `account add`\ntakes a Linear API key: `--token-stdin` stores it under the cotal home as a 0600 file, and\n`--token-file` points at an existing file that must not be readable by other users. The file is read\nat each use, so rotating it needs no restart. `account login` runs Linear's OAuth flow instead: it\nregisters a client, prints the authorize URL, and waits on a loopback redirect. It asks for scope\n`read` in `readonly` mode and `read write` in `write` mode, keeps the tokens in a 0600 file, and\nrefreshes them before a request is sent. OAuth requests only go to `https://mcp.linear.app`.\nRedirects are refused so the credential is never forwarded.\n\n`inventory` reads the server's capabilities and every page of its tools, plus resources, resource\ntemplates and prompts when the server advertises them. Names and schemas are printed as the server\nsent them. The digest covers the whole inventory; pass it to `--inventory` so a call is refused\nbefore dispatch if the inventory changed. Capabilities this command does not represent, such as\nresource subscriptions or logging, are listed under `unsupported`.\n\nBare `linear` and `linear account` show their scoped operations without opening an account or\ncontacting the upstream. Account add/show, inventory and the native call/resource/prompt forms\nsupport shared rich/plain reports and version-1 JSON envelopes. Scoped help and completion use\nthe same operation/flag declarations as validation. Extra operands and wrong-mode flags refuse\nbefore account access. Positional values are bounded to 8192 characters without terminal controls.\nPiped tokens are bounded to 1 MiB of strict UTF-8; their contents and argument parse errors are\nnot echoed. Account reports describe recorded configuration or a native save acknowledgement,\nnot current access to Linear. An account name with no record refuses with `TARGET_NOT_FOUND` and\nthe `account add` route. A record that exists but cannot be read refuses with\n`DEPENDENCY_UNAVAILABLE`.\n\n`inventory --json` retains the full native inventory inside the result envelope. Human reports\nshow at most 60 whole tool rows. Read-only hints remain upstream declarations, not enforcement or\nwork progress. `call`, `resource` and `prompt` retain the complete native reply under\n`result.reply` with `--json`; rich/plain output shows at most 60 whole result lines. The bearer\nis scrubbed by the native client; other upstream content is not claimed to be secret-redacted.\n\nA tool result with `isError: true` remains an MCP result (exit 0), with a visible warning. This does\nnot assert that the requested work succeeded. A native refusal with outcome `not-executed` returns\nexit 2; that includes a deadline that expires while queued, discovering or connecting. An unknown\nprotocol/transport outcome returns exit 3 without replay, including for read-only requests. The\nnative outcome and reason remain in the result. Errors carry a status or class, not HTTP response\nbodies. An interrupted account save or OAuth flow can return unknown because private state or an\nexternal authorization may already have changed.\n\nOAuth login keeps the native human authorization flow, so it offers plain output but not JSON\nand refuses when input is disabled. The existing foreground `serve` route keeps its native authority and human output; it does not\naccept finite-output flags. The existing `caller` route\nkeeps its native static issuer but supports shared JSON/plain private-file receipts. Its output\nmust be outside managed state in an existing protected physical parent. An existing destination\nrefuses before provisioning. Publication is create-only under the cooperative output lock, and\nuncertain provisioning/publication is not retried. It prints neither credentials nor environment-\nexport launch recipes. The preferred supported standalone path is now `auth mint --endpoint`\nafter a live digest-verified surface is available. No caller or managed launch is inferred from an\naccount, inventory or reply.\n\n### Serving the endpoint\n\n`serve` registers the account as an endpoint named by `--endpoint` and serves it until Ctrl-C. The\nname must be a reverse-DNS name in a namespace you own, such as `com.example.linear`; it is\nauthorized for this mesh's local owner only because you configured it. `serve` reads the inventory\nfirst, so an account that cannot be reached is never registered. It then walks the same path every\nregistered endpoint does: it publishes the contract to the contract store, opens the issuance gate,\nregisters a fresh instance, writes its ready status, and mints a scoped serve credential through\nthe gate. Every step runs on short-lived executor credentials scoped to this one endpoint instance.\nThe space signer stays in the `serve` process for renewal at 75% of the credential's life and is\nnever written out, printed, or handed to the serving connection. Ctrl-C stops serving, closes the\nLinear session and removes the instance's service record, each within 10 seconds. A start that\nfails after registration removes the record before it exits and says whether that removal\ncompleted; a registration that fails part-way rolls its own record back and reports when it could not.\n\nThe endpoint serves five fixed commands: `inventory`, `call-tool`, `read-resource`, `get-prompt` and\n`complete`. Each needs the one `linear.mcp` capability. That capability is permission to call the\nendpoint; which tools a call can reach is decided by the Linear account and its mode. `inventory`\nreplies in pages that fit a broker message: the first page carries the server, its capabilities,\nits instructions and per-section counts, and every page carries entries verbatim with an opaque\ncursor bound to the inventory digest. An entry is never cut: discovery refuses an inventory whose\nfirst-page head (server, instructions, capabilities) or any entry prints over 48 KiB the way an\nagent tool prints it (indented JSON, which grows with nesting), naming the entry, so the first page\nand every entry it accepts fit one reply and the tool an agent reads it with. A cursor or `inventoryDigest` from an older inventory is refused before anything is sent to\nLinear. `call-tool`, `read-resource`, `get-prompt` and `complete` require the digest the caller read.\n\n### Callers\n\n`caller` provisions an isolated caller on a static-auth mesh: an `agent` credential with its own\nmailboxes and request rows for only the five Linear commands on that endpoint, plus the channels\nin `--channels`. It carries no spawn, run, admin or provisioner capability. The credential is\nwritten 0600 to a create-only `--out` outside managed state and never printed. Its physical parent\nmust already exist and be protected. The shared receipt contains the native identity, lifecycle,\nexpiry and acknowledged footprint, not credential bytes. An existing output refuses before\nprovisioning. A failure after native issuance starts stays unknown with confirmed identity/file\nreceipts retained, and is not retried. The command does not print environment-export launch recipes.\n\nThis remains the native standalone caller exporter, not a connector setup or generic caller bundle.\nA supported bootstrap must carry its actual credential and minted lifecycle together; a file alone\nis not a native caller identity. No seat is started, and managed static `cotal spawn` agents still\nrefuse endpoint capabilities. `auth mint --endpoint` now covers supported untargeted synchronous\nsurfaces through live native resolution. Targeted/journaled/action issuance and caller/launch\nadapters are not supported.\n\nA per-user-auth mesh is refused by both `serve` and `caller`: there, endpoint and caller credentials\ncome from the remote auth service, which this package does not support yet. Nothing falls back to a\nlocal signer. An open mesh is refused too, because it has no credentials to scope.\n\nNot represented yet, and listed under `unsupported` when the server advertises them: resource\nsubscriptions, logging, progress notifications and experimental capabilities. The server cannot\ncall back for sampling, elicitation or roots. Every call acts as the account's one Linear identity.\n\n## pending\n\n```bash\ncotal pending <agent> [--durable <exact-dm-durable>] [--limit <1..100>] [--space <s>] [--json|--plain]\n```\n\nThe finite read reports the selected DM durable's native pending count, ack\nfloor, delivered sequence, frontier, retention limits and up to 20 candidate\nIDs by default. JSON and plain output use the common one-document result. A\nname must match one observed roster entry; an exact principal ID wins over a\ndisplay-name match, and an ambiguous display name refuses. `--durable` reads a\nprevious lifecycle by its complete durable name without substituting a\nsame-named current agent. An absent or unreadable durable is not `pending 0`.\nThe caller needs the native admin read authority; user-auth has no equivalent\nview here and refuses without a static or bare fallback. A failed read does\nnot prune a locally saved mesh registration.\n\nCandidates are read without acknowledging DMs. The transient read consumer is\ndeleted after the bounded pass; an unconfirmed cleanup or candidate read is\nreported as partial. Candidate IDs are not evidence of delivery, a missing\nmessage or complete history. Recipient contents are not printed. The retained\n`deliver pending` spelling keeps its earlier output shape.\n\n## deliver\n\n```bash\ncotal deliver [--space <s>] [--server <url>] [--tls] [--creds <file>] [--root <dir>] [--shard <n>] [--shards <n>] [--dev-mint]\ncotal deliver pending <name> [--limit <n>] [--durable <name>] [--json]\n```\n\nWith no positional, `cotal deliver` runs the delivery daemon (see\n[the delivery daemon](delivery-daemon.md)). `deliver pending <name>` never starts the daemon: it\nis an operator-only read over one recipient's DM durable, for the moment after a send when the\nquestion is \"what does the broker actually hold for them.\" It resolves `<name>` against a short\npresence watch (an `offline` card still counts, since the recipient may be dead, that is what\nthe verb exists to inspect); when neither a card nor the durable can be found, it prints\n`\u2717 not-found: no agent \"<name>\" and no DM durable for it in space <s>` and exits non-zero, never\n`pending 0`. On a match it prints the durable name and one fact per line: `pending`,\n`ack-pending`, `delivered`, `ack-floor`, `created`, `frontier`, and the stream's `max_age` /\n`max_msgs_per_subject` / `discard` limits (`--json` prints the same facts as one object), followed\nby a bounded, unacked read of up to `--limit` (default 20) recent candidate message ids under the\nheading `recent candidate ids (from the ack floor; not proof of a hole)`, a list of what is\nthere, not proof that nothing was lost.\n\nThe verb needs the `admin` credential profile: it runs through the same static-mesh route as\n`cotal auth mint --profile admin`, and refuses a user-mode mesh, naming the retired static credential,\nbecause there is no user-mode inspection authority yet. Pass `--creds <file>` for an off-registry\nadmin credential. A same-name respawn never inherits a predecessor's held DMs (the durable is\nlifecycle-keyed); an old lifecycle's durable is reachable only by the name a live read printed\n(the `<durable>` line on the first line of this verb's output). Pass that name with `--durable\n<name>` to read it directly once the lifecycle's card is gone from the roster. This skips the\npresence watch on `<name>` entirely, so `<name>` is required but only echoed in error text.\n\n## auth mint\n\n```bash\ncotal auth mint <name> [--profile agent|observer|admin] [--out <private-file>] [--json|--plain]\ncotal auth mint <name> --provision [--role <role>] [--space <space>] [--server <url>]\ncotal auth mint <name> [--expires-in <seconds> | --expires-at <unix-seconds>]\ncotal auth mint <name> --identity <private-creds> --out <private-file>\ncotal auth mint <name> --endpoint <class> --out <private-file> [--channels <a,b>] [--expires-in <seconds>]\n```\n\nThe fixed-profile forms use the existing native static credential profiles. No generic host\nprofile is exposed.\nOpen and per-user-auth spaces refuse; a cached login does not replace signing authority. The\nresolved root supplies the persona ACL, signer and default storage together. A root that cannot be\nresolved refuses with its next step, such as `cotal up`. Conflicting current-folder and\nselected-root accounts refuse before issuance.\n\n`agent` is the default profile. Its channel ACLs and role come from the selected root's persona\nwhen present; explicit `--allow-subscribe`, `--allow-publish` and `--role` override them. An empty\nACL flag means no explicit entries. The native read-ACL resolver still enforces boot membership.\nObserver and admin have fixed native scope and reject agent ACL, role and provisioning flags\nrather than implying those inputs narrowed anything.\n\nThe default credential is created through its canonical secret-store kind. An existing label is\nnot overwritten. Use another label or an explicit private output. Explicit exports stay outside\nmanaged `.cotal` and workstation state. A pre-existing explicit output must be an owned private\nnative credential matching the nkey deliberately supplied through `--identity`; unrelated files\nand identities refuse. New outputs use create-only publication. The output's directories follow\nthe [`space create`](#selecting-a-space) path rule, which also accepts a root-owned sticky directory\nsuch as `/tmp`, and the file is owner-only. Unsupported platforms refuse rather than weakening it.\n\n`--identity` reuses the native nkey. The out-of-band agent mint reports its newly allocated\nlifecycle UID, with a warning that it did not reuse the old inbox/delivery footprint. Use that\nreported UID for the resulting client; do not infer it from the label or assume the old UID was\npreserved. No private nkey or JWT appears in a receipt.\n\nWithout `--provision`, fixed-profile minting writes credentials only. `--provision` uses one native provisioner\nconnection to pre-create that lifecycle's bind-only DM/delivery and role footprint. It performs\nthe authenticated preflight against the same recorded broker and TLS requirement, without\npruning a registration. An acknowledgement proves neither a joined agent nor task completion.\nA partial or unknown result retains the allocated public identity and lifecycle for diagnosis\nand does not retry the mint.\n\nLifetime inputs use native seconds. The receipt reports the resulting JWT expiry, rather than\npromising a default lifetime. `--expires-in` and `--expires-at` are mutually exclusive. JSON is\none finite document; plain output uses the same public receipt. Signing material is exported by\n[auth signer](#account-signer-export), not a signer flag on mint. The old top-level `mint` spelling\nis refused with the literal namespace replacement.\n\n### Endpoint caller credentials\n\n`--endpoint` is a separate closed mode, not an ACL override on an observer or admin profile. It\nrequires a private `--out` in an existing protected physical parent outside managed state. Only\n`--channels`, `--expires-in`, space/server and presentation flags combine with it. An empty or\nomitted channel list grants no explicit read/post channels. Supplied channels deliberately enter\nboth ACLs, as in the native standalone Linear caller form. No persona ACL or role is inferred.\n\nThe same static-root/account checks run first. The existing native control instrument then resolves\na live endpoint and digest-verifies its visible command surface. This issuer accepts only nonempty,\nbounded untargeted synchronous ephemeral surfaces. A targeted, journaled or action command refuses\nthe whole issuance before footprint work; unsupported rows are not silently dropped. The result\nrecords the observed responder and contract digests, not a promise that every future incarnation\nhas the same implementation. The caller is scoped to the class's observed command names, not that\nsingle answering instance.\n\nThe native agent provisioner creates the new lifecycle's bind-only footprint and additional endpoint\nrequest rows. No spawn, run or admin capability is added. The ordinary native agent baseline remains;\nthis is not a new hand-written permission profile or a managed-seat launch adapter. User-auth/open\nspaces still refuse. The selected broker's required TLS is retained for discovery and provisioning.\n\nAn immutable owner-private `<out>.issuance.json` intent precedes the first footprint operation.\nIt contains public identity/lifecycle, requested scope and contract coordinates, never credentials.\nIts presence is not a grant or successful submission receipt. An existing output or retained intent\nblocks another issuance, even if no output was published. Partial/unknown provisioning retains that\noriginal intent and does not replay the native call. Success reports native footprint acknowledgement\nand the exact privately published file; provisioning is not presence, task completion or cleanup.\nTemporary provisioner close is reported separately and can leave a partial result.\n\nRich/plain receipts show the actual caller/lifecycle, described endpoint owner and responder,\nexplicit channel scope and native expiry. Command tables show at most 40 rows with an omission\ncount; JSON retains every command and contract digest. The local issuance request is not a broker\nrequest acknowledgement.\n\nNo credential bytes or environment-export recipe are printed. Endpoint describe/invoke consume\nthis paired file under the same registered static root:\n\n```bash\ncotal ep describe ep:<class> --space <space> --creds /absolute/private/caller.creds\ncotal ep invoke ep:<class> <command> --space <space> --creds /absolute/private/caller.creds --args '<json>'\n```\n\nKeep the matching `<private-file>.issuance.json` beside the credential. The consumer checks both\nprotected snapshots and validates the selected root's full static trust chain. The native JWT\nsignature, matching user seed, current validity window, account and current delegated signing key\nare checked before connecting. A retained older signing key is not guessed from a different root.\nThe caller coordinates and recorded commands must reproduce the signed publish/subscribe/response\npermissions and principal tags through the native builder. The unsigned local intent selects that\ncomparison; it grants nothing and proves no submission, lifecycle currency or completion. Original\nresponder/contract metadata remains an issuance observation, not an instance or future-code pin.\n\nThe connection retains registered TLS and uses only that file. Its broker admission is separate\nfrom local checks. Invocation accepts only an included untargeted synchronous ephemeral method\nfrom the fresh digest-verified surface. A changed targeted, journaled or action shape refuses before\npublication. Instance pins, scatter, admin, agent targeting and goal-follow flags are unsupported\nfor this caller mode. Calls do not repair a binding, switch authority or resubmit an unknown effect.\n`--server`, when supplied, must repeat the registered broker. Target/reference selection is\nread-only, including when a paired file is selected. Receipts retain the public caller binding;\ndescribe shows methods outside the file scope without granting them. Issuance next steps keep\nexplicit file selection instead of switching back to the normal operator instrument.\n\nArbitrary raw files, issued-generation files and different native profiles are not interpreted as\nthis standalone caller. User-auth issuance and managed static launch are not supported.\n\n## Login\n\n```bash\ncotal login --idp <auth base URL> [--client-id <id>] [--plain]\ncotal logout --idp <auth base URL>\n```\n\nSigns you in to a per-user-auth mesh's IdP (device code flow) and caches the session; run it\nonce per machine. `--plain` presents the approval prompt and signed-in result without color.\nDevice approval is still interactive and has no native noninteractive or JSON result in this\nclient; `--no-input` refuses before starting a provider request. The IdP URL must use `https://`.\nPlain `http://` is accepted only on a loopback IP literal such as `127.0.0.1` or `::1`, for an IdP on this machine. `localhost` is refused because it\nis a name: a hosts entry would choose the IdP. It prints your IdP subject, the id the operator\ngrants against. When the trusted\n`/token` response advertises a same-origin space catalog, login validates and records that account's\nspaces immediately. After a\nlogin, every command on that mesh works under your identity: each connect takes a fresh IdP\nproof, exchanges it locally for a short-lived bearer, and is authorized against the actor\nledger at connect time. A provider that cannot be reached, or that stops answering, refuses with\n`DEPENDENCY_UNAVAILABLE` (exit 2) and caches no session. `logout` revokes the IdP session, clears\nits cache, and removes only that\naccount's discovered registry entries. See\n[identity & auth](identity-and-auth.md).\n\n## actor\n\n```bash\n# an upsert of the WHOLE row: name all three ACL flags, or pass --full for the wide defaults below\ncotal actor grant <actor> --sub <IdP subject> --scope a,b --allow-subscribe a,b --allow-publish a,b [--role <r>] [--label <l>]\ncotal actor grant <actor> --sub <IdP subject> --full [--scope a,b] [--allow-subscribe a,b] [--allow-publish a,b] [--role <r>] [--label <l>]\ncotal actor revoke <actor> (--sub <IdP subject> | --owner <u_\u2026>)\ncotal actor list\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | the resolved mesh | Space whose ledger to manage |\n| `--sub <subject>` | none | The IdP subject (shown by `cotal login`) the actor belongs to |\n| `--owner <u_\u2026>` | none | The derived owner token (alternative to `--sub`) |\n| `--full` | off | Fill each ACL flag left off with its wide default; without it, `grant` refuses unless all three are named |\n| `--scope <a,b>` | `spawn,role:default` with `--full` | Capability scope (`''` = none; `spawn` = may run agents; `role:<r>` = may delegate role r; `admin` = cross-agent control; `supervise` = eligible for the closed remote manager-service view when the host enables it) |\n| `--allow-subscribe <a,b>` | `>` (all channels) with `--full` | Channel read ACL; the user's envelope, their agents can never read beyond it |\n| `--allow-publish <a,b>` | `>` (all channels) with `--full` | Channel post ACL; also the envelope for their agents' posting |\n| `--role <r>` | none | Role (scopes the task-queue consumer) |\n| `--label <l>` | none | Display label for `actor list` (never the IdP subject) |\n| `--parent <owner.actor>` | none | Audit link to the spawning principal. An operator grant is the authority, so the link does not narrow the row |\n\nThe actor ledger is the single authorization source of a user-auth space: no row, no access.\n`actor list`, `grant` and `revoke` resolve their space and root as `auth grants`, `auth grant` and\n`auth revoke` do, and refuse the same way. With no space to resolve they refuse with a next step,\nsuch as `cotal up`, rather than assume one, and a root that does not hold the space's user-auth\nledger refuses `list` rather than report no rows. On a static or open space the refusal says that\nactor grants apply only to a space started with `cotal up --user-auth`.\nA usage error refuses with exit 1 before any space or authority is read, and one refusal names every\nmissing input: `--sub` or `--owner`, and for `grant` the ACL flags left off without `--full`.\n`grant --full` is the **full** envelope (all channels; scope `spawn,role:default`, so it may spawn and may delegate the default role). A\n`grant` that leaves off `--scope`, `--allow-subscribe` or `--allow-publish` without `--full` is\nrefused and writes nothing. A re-grant **replaces the whole row**, not the one field you name, so to add a capability spell\nevery field out: the new scope plus the row's current read set, post set, role and label\n(`cotal actor list` shows what a row holds). Under `--full`, a field left off does not stay as it\nwas: it reverts to the wide default in the table above. A re-grant retires the current interactive lifecycle through the running auth\nservice before it rotates the row, so copied bearers cannot cross an authorization update. If that\nretirement cannot be confirmed, the row is left unchanged and the command fails with the recovery\naction. `revoke` uses the same retirement before deleting the row, which lets a later grant create a\nreal successor instead of colliding with a live predecessor. `supervise` is separate from `spawn` and `admin`: it only makes a signed-in\nperson eligible for the host-provided closed remote manager-service view; it does not grant\nmanagement of another owner or a general host profile. `revoke` denies the next exchange and\nthe next connect with no restart, and evicts the principal's live connections. It reports\n`verified gone` only when the delivery daemon's eviction result names the principal and shows a\ncomplete scan with no connection left; any other result prints `live-connection eviction INCOMPLETE`\nwith its counts, or with the reason the result was not accepted. Managed-agent rows\n(written by the spawn path) live in a disjoint row space this command never touches. See\n[identity & auth](identity-and-auth.md).\n\n## doctor\n\n```bash\ncotal doctor auth [--space <space>] [--fix] [--json|--plain]\n```\n\nThis inspects the current root's local credential claims and recorded renewal state. It does not\nprove current broker authorization, daemon adoption, connected agents or task progress. The shared\nreport keeps full credential paths and public issue/expiry metadata; JSON retains all diagnostic\nrows without JWTs, seeds, fingerprints or raw parser/renewal error bodies. On a root that holds several\nspaces, `--space` names one; it defaults to the sole space. Without `--space`, a root with several\nspaces refuses with `TARGET_AMBIGUOUS` (exit 2) and names them. In a folder whose root holds no space,\nthe report is partial (exit 4) and its next step is to run it in a space's root, which `cotal space`\nlists.\n\nPlain diagnosis starts no repair or connection. A required legacy credential or user-auth directory\nmigration refuses instead of silently moving files or reading canonical absence as an empty scope.\nA credential with no expiry claim is a finding whatever its kind, since a copy stays valid until its\nsigner rotates. `--fix` re-signs only the static daemon credentials, so an unbounded agent credential,\nsuch as one `auth mint` wrote without `--expires-in` or `--expires-at`, stays a finding until it is\nreplaced with a bounded one.\nMissing local signing trust is not inferred to mean an open or healthy mesh. An unreadable renewal\nrecord prevents a complete healthy verdict. Locally ordered older incarnation files are labeled as\nfile observations, never proof of a live successor or permission to delete or respawn an agent.\n\n`--fix` explicitly permits native local material migration and the supported static daemon re-sign\nphase. Its path-move and re-sign acknowledgements are retained separately. It holds the native\nrenewal lease when using the broker and refuses another holder. Offline repair is labeled as\nuncoordinated; an authenticated preflight failure is not called an offline target. User-auth roots\ncannot receive a static-signer repair fallback. No system account is rotated and no agent is\nrespawned or silently given a new identity.\n\nA local re-sign does not erase a recorded broker refusal or claim that a daemon adopted it.\nA recorded refusal names the space's manager, the renewal owner, as the next step. A pre-per-space\n`.cotal/renewal.json` names no space, so it is reported as a leftover and never read as the space's\nverdict.\nRemaining findings, incomplete audit reads and partial/unknown repair outcomes keep classified\nfinite receipts. Inspect the original renewal owner and lifecycle rather than repeating a mutation\nor creating replacement agents merely to obtain a different report.\n\n## join\n\n```bash\ncotal join --space <s> --name <n> [--role <r>] [--channel <c>]\ncotal join --link <url> | --token <t>\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which mesh, and which credential |\n| `--name <n>` | none | Your presence name |\n| `--role <r>` | none | Your role |\n| `--channel <c>` | none | Channel to join |\n| `--kind <k>` | `agent` | Endpoint kind |\n| `--link <url>` | none | Join link (`cotal://\u2026`) |\n| `--token <t>` | none | Join token |\n| `--lifecycle-uid <uid>` | none | Required with `--creds`: the lifecycle UID minted alongside the credential (`COTAL_LIFECYCLE_UID` works too). A credential's durable grants name exact lifecycle-keyed resources, so `join` refuses to invent one |\n| `--tls` | off | Connect over TLS |\n\nAn interactive presence: join a space under your own name and role, without launching an agent\nharness. A `--link` or `--token` supplies the where and the auth in one value. See\n[Spaces](spaces.md) and [Identity and auth](identity-and-auth.md).\n\nOn a registered user-auth space, sign in with `cotal login` first. `join` uses your existing `cli`\nactor bearer and renews it through the same login and exchange while the session runs. Its channel\nand DM resources must already be provisioned by the space operator. It never mints static\ncredentials or widens your grants. A changed principal, lifecycle or connection trust refuses\nrenewal and requires leaving and joining again. A revoked login is reported by renewal, and the\ncurrent connection can last only until its already-issued bearer expires.\n\nFor `nats://` and `tls://` addresses, if TLS is required but the broker does not offer it, `join`\nrefuses with a request for the operator's TLS-enabled broker address. It never retries over\nplaintext. A `ws://` address uses plaintext WebSocket. Use `wss://` for TLS.\n\n## Manifest deploys\n\nA `cotal.yaml` manifest declares a whole mesh (channels, personas, roles, and ACLs) in one file.\nThese commands consume it:\n\n```bash\ncotal up -f cotal.yaml # boot a fresh mesh from the manifest\ncotal team deploy cotal.yaml # deploy the manifest additively onto a running mesh\ncotal team remove <deployment-id> # retire a returned deployment\ncotal topology view -f cotal.yaml # validate + view the access graph, change nothing\n```\n\n`up -f` brings up a new broker and applies the manifest. `team deploy` requires a registered,\nrunning mesh and applies additively: all-new live channel-only manifests, or bounded all-new agent\nand mixed manifests on static-auth or open meshes through one native manager. It does not launch\nuser-auth agents. `team remove` retires a returned deployment by its full ID within the limits in\n[Recorded team views](#recorded-team-views). `spawn -f` no longer deploys a manifest; an additive\nledger written by that older path is torn down with `cotal down -f <file> --run <id>`. `up -f`,\n`team deploy` and `team remove` take `--dry-run` to print the plan without mutating anything.\n`topology` validates the manifest and renders its channel / role / ACL graph. See\n[Define a team](define-a-team.md) and the [manifest reference](manifest.md).\n\n## ext\n\n```bash\ncotal ext [list] [--json | --plain]\ncotal ext add <npm-package>\ncotal ext remove <name>\ncotal ext root # print just the install prefix (scriptable)\ncotal ext seed [--repair|--reset|--force]\n```\n\nOperator-installed extensions: `add` installs an npm package into a cotal-owned prefix and records\nevery registry provider it contributes. Commands appear in help, completion, and dispatch; runtime\nproviders are lazy-loaded by commands such as `supervise`; local process providers participate in\n`status` and selective `down`. `remove` and `list` manage them. The `@cotal-ai/web` dashboard is the\ncanonical command/process example. Installed packages and their location are described in\n[config](config.md). When a package needs an export its linked `@cotal-ai/*` peer does not have,\n`add` rolls back and names which install is behind, as a later load of an installed one does.\n\nBare `cotal ext` lists the inventory, headed by the install prefix. That prefix is a cotal-owned npm\nroot kept **separate** from npm's own global tree. These packages never show up in `npm list -g`,\n`cotal ext` (or the Extensions section of `cotal status`) is the canonical inventory. `cotal ext root`\nprints only the path, for scripts. The versions shown are the manifest pin recorded at add time.\nOn POSIX, the extension writer creates a missing Cotal config directory with mode `0700`, even\nwith a group-writable umask, and a missing install prefix with mode `0755` and its `package.json`\nwith mode `0644`. Its npm children start with 022 added to your umask. Existing directory\npermissions are not changed. Completion still refuses a pre-existing writable ancestor rather than\nweakening its shell-file ownership checks.\n\nBare `ext --json` (or `ext list --json`) writes one JSON document: `result.root` is the install\nprefix and `result.items` holds one row per installed extension with `pkg`, `version`, `spec` (what\n`ext add` was given), `seeded` (`true` for an entry the built-in seed installed) and `provides` (the\n`kind:name` refs the table shows). An empty prefix is an empty list. `--plain` writes the same report\nwithout color. The other `ext` subcommands refuse `--json` and `--plain` with `USAGE_INVALID`.\n`ext root` stays a raw path for `$(cotal ext root)`.\n\nAn installed package whose directory is gone is reported as not installed. One whose `package.json`\nis not JSON or has no `version` is reported as corrupt, naming the package and its `package.json`.\n\nRemoving an extension that owns a running local process is refused with the mesh root and its\n`cotal down <component>` command; stop it first so uninstalling the package never strands a process\nwhose lifecycle provider is gone.\n\nLoading an installed extension takes the same locks as `add`, `remove` and `cotal update`. When another\n`cotal` process holds them, the load waits up to five seconds in total (one deadline for both locks)\nwithout blocking the waiting process, then refuses. A running `cotal update`, `cotal ext add` or\n`cotal ext remove` holds the update-pass lock as well as the install/remove lock, so the load refuses with\n\"another extension update or mutation is in progress (pid N) - retry once it finishes\". The install/remove\ntext, \"extension install/remove is in progress (pid N) - retry after the active `cotal ext` command\nfinishes\", appears only when the update-pass lock is free but a live process still holds the\ninstall/remove lock itself, for example an update child that outlived its `cotal update` parent. Re-run\nthe command once the other one has finished.\n\n### Built-in connectors are seeded extensions\n\nThe first-party agent connectors (`claude`, `opencode`, `codex`, `hermes`, `jcode`, `pi`) are not compiled into\nthe binary. They are seeded on first run through the **same** `ext add` path a third party uses, and\nappear in `cotal ext list` like any other extension. So you can remove one you do not want\n(`cotal ext remove @cotal-ai/connector-hermes`), and a deliberately-removed connector STAYS removed\nacross upgrades. `cotal ext add <your-package>` adds a third-party connector the same way. The web\ndashboard (`@cotal-ai/web`, providing `service-preset:web`) is the seventh built-in seeded on the\nsame path.\n\n`cotal ext seed` is the maintenance entry for that seeding (it runs automatically before the first\ncommand of each boot that is not a read, so you rarely call it). A read, such as `status`, `ps`,\nbare `use`, `ext list` or `docs`, installs and refreshes nothing. An installed extension's `reads` declaration is\ncached when it is added. A bare read and a read subcommand skip that seed prelude; a subcommand\nwithout the declaration still seeds. After an upgrade, a read that would load a\nbuilt-in still at another version names the package and its version with `cotal ext seed` instead\nof loading it. Each seeded connector's `\u2713 added` line goes to stderr, so the command that triggered\nthe seed keeps stdout to itself:\n\n| Flag | Meaning |\n|---|---|\n| (none) | Reconcile: seed any never-seeded built-in, refresh a seeded one whose version the binary bumped, leave a removed one removed. A no-op once current. |\n| `--repair` | Recover after an interrupted seed or a lost authority (rebuilds the interrupted connector; restores the removed-vs-never-seeded record from its durable backup). Also reinstalls a seeded built-in whose entry file is gone or whose `package.json` is not JSON or has no `version`. |\n| `--reset` | Discard the record and re-seed all seven built-ins (the six connectors plus the web dashboard). **Resurrects any you removed.** Rebuilds cleanly over corrupt seed state. |\n| `--force` | Re-seed the built-ins even when the version stamp is current. Refuses to downgrade a newer shared store. |\n\nWhen a newer `cotal` advances the operator-global seed store to its generation, it prints one\nmigration line naming the old and new generations, the exact CLI entry that wrote the store, the\ncommit timestamp, and `seed/stamp.json`. That writer and timestamp are kept in the stamp, so a later\nolder CLI refusal can say which executable wrote the generation it will not overwrite and when.\nLegacy generation-only stamps remain readable; their refusal simply has no writer provenance to add.\n\nAn older `cotal` refuses a seed store written by a newer version. When it can verify a sufficient\n`cotal` executable on PATH or at the installer's `~/.local/bin/cotal` location, the refusal names\nthat absolute path so a reduced service PATH does not select the older binary again. Otherwise it\nkeeps the generic newer-version instruction. Every maintenance mode, including `--force` and\n`--reset`, refuses to replace a newer shared generation. Run the newer executable instead.\nSame-generation force remains supported. Reset still repairs corrupt state and resurrects\nremoved connectors. Older released executables may still allow a backward rebuild; do not use\ntheir force advice to recover from selecting the wrong installation.\n\nA source-checkout CLI (`pnpm cotal`, `tsx bin/cotal.ts`, `node bin/cotal.ts`, or a suite child of\nthose) refuses to write or garbage-collect that store. The refusal names the path, the generation\nit declined, and `COTAL_SKIP_CONNECTOR_SEED=1` as the way to run other commands from a checkout,\nbecause pointing `$XDG_CONFIG_HOME` at a scratch dir alone does not lift it. With the skip set, a fresh\nconfig gets no built-in connectors (`cotal ext list` shows none), so add one from the checkout with\n`cotal ext add <checkout>/extensions/connector-claude-code` against that scratch `$XDG_CONFIG_HOME`. `COTAL_HOME` does not relocate this\nstore. An entry that cannot be proven as a released install is refused the same way. Isolated\nrelease tests that must seed from a checkout-shaped `bin/` set `COTAL_ALLOW_CHECKOUT_SEED=1` after\npointing `$XDG_CONFIG_HOME` at a scratch dir; that override is documented here, not on the refusal\nline. An opt-in write still records the checkout path in `seed/stamp.json` as `writtenBy`.\n\nThe default connector for a bare `cotal spawn` (no `--agent`) is the persona's `agent:` pin if it\nhas one, else `claude`; set `COTAL_DEFAULT_AGENT` or `cotal config set harness` (e.g. `opencode`)\nto change the fallback. It is a default, so a persona that pins its harness still wins over it. An `--agent` naming a removed\nconnector fails loud with the exact\n`cotal ext add` to restore it. Set `COTAL_SKIP_CONNECTOR_SEED=1` to turn off the automatic first-run\nseed/refresh entirely (for a controlled or offline setup that manages connectors by hand); `cotal ext\nseed` still runs on request. `cotal agent-bearer` never takes the seed at all: it is exec'd by\nspawned seats on every bearer refresh, so it neither reconciles nor is refused by the store's\ngeneration (see [Plumbing](#plumbing)).\n`cotal completion` skips the seed as well, so a shell profile that sources its stub never reconciles\nand is never refused by the store's generation or the source-checkout guard. Printing a stub writes\nnothing, and `completion install` writes only shell configuration.\n\n## docs\n\n```bash\ncotal docs [<page>] [--refresh] [--json | --plain]\ncotal docs --search <query> [--json | --plain]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--search <query>` | none | Show the sections that best match a query, each naming its page |\n| `--refresh` | off | With a page: also try the version-pinned copy on docs.cotal.ai |\n| `--json` / `-j` | off | One JSON document |\n| `--plain` | off | The same report without color |\n\nReads the docs bundled with this installed version, offline: the same pages agents read with\n`cotal_docs`. Bare `docs` lists the pages by category (`Start here`, `Concept`, `Guide`, `Reference`,\n`Project`), each with its title; `result.pages` holds each page's slug, title, kind and summary, and\n`result.sources` the normative `spec`, `lang` and `schema`. A page is named by its slug,\nignoring case and a trailing `.md`; `result.body` holds its Markdown. An unknown page is refused with\n`USAGE_INVALID`, naming every page.\n\n`--refresh` fetches the page from the immutable path for this version on docs.cotal.ai.\n`result.source` says which copy was served, `bundled` or `docs.cotal.ai`, and `result.refresh` says\nwhat the fetch did: `served`, `not-published` (the path has no copy) or `unreachable`. The bundled\ncopy is served whenever the fetch does not succeed. `--refresh` applies only to a page.\n\n`--search` ranks sections by the words of the query. A search with no match is an empty result.\n\n## config\n\n```bash\ncotal config [--user | --space <name>] [--json | --plain]\ncotal config get <key> [--user | --space <name>] [--json | --plain]\ncotal config set <key> <value> [--user | --space <name>] [--json | --plain]\ncotal config unset <key> [--user | --space <name>] [--json | --plain]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--user` | your defaults for a write | Your own values, with no space's values |\n| `--space <name>` | the selected space for a read | Your values for one space |\n| `--json` / `-j` | off | One JSON document |\n| `--plain` | off | The same report without color |\n\nYour [preferences](config.md#preferences): `harness`, `persona`, `detach-key`, `color` and `editor`.\nBare `config` lists every key with the value a command uses, where it came from (a variable, your\nvalue for the space, your own value, or the default) and the values it outranks. `get` shows one\nkey. A read looks through `--space`, else the space selected with `cotal use`, else your defaults;\n`--user` reads your defaults alone.\n\n`set` and `unset` change your defaults, or your values for one space with `--space`. Only `harness`\nand `persona` have per-space values. A write that a variable outranks in this shell is still saved,\nwith a `PREFERENCE_SHADOWED` warning naming the variable. Setting a value that is already saved, or\nunsetting one that is not, is a `no-op` and exits 0. An unknown key or an invalid value is refused\nwith `USAGE_INVALID` and the valid choices, and a write that lost a race with another writer is\nrefused with `CONFLICT`.\n\n`result.settings` (or `result.setting` for `get`) holds each key's `value`, its `winner`\n(`{source, name, value}`, where `source` is `environment`, `space`, `user` or `default`) and the\n`shadowed` layers below it. A write's result has the `previous` and new `value`, the file's new\n`revision`, and `effective`, the key as a command there now resolves it.\n\n## completion\n\n```bash\ncotal completion [--json|--plain]\ncotal completion script <bash|zsh|fish|powershell>\ncotal completion install [shell] [--json|--plain]\ncotal completion uninstall [shell] [--json|--plain]\n```\n\nBare `completion` reads local integration files. It reports the script version, startup reference\nand recorded ownership separately. It does not inspect or load the running shell. An unreadable\nfile or ownership record produces a warning and a partial result, exit 4. A relative `ZDOTDIR`\nis resolved from the current directory for this report, as zsh resolves it.\n\nIn zsh, initialize its completion system with `autoload -Uz compinit && compinit` before\nsourcing the native zsh stub.\n\nLoad completion in the current shell:\n\n```bash\nsource <(cotal completion script bash)\nsource <(cotal completion script zsh)\ncotal completion script fish | source\ncotal completion script powershell | Out-String | Invoke-Expression\n```\n\n`completion script` writes raw shell code and takes no `--json` or `--plain`. Generation is\noffline. The stable hidden `__complete` entry reads registered command specifications and local\npersona files. It does not contact the mesh or refresh extensions.\n\n`install` and `uninstall` take a shell name or detect it from `SHELL`. Pass `powershell`\nexplicitly when PowerShell inherited another shell's `SHELL`. On supported POSIX hosts:\n\n- Bash uses `~/.config/cotal/completion.bash` and an owned block in `~/.bashrc`.\n- Zsh uses `~/.config/cotal/completion.zsh` and an owned block in `$ZDOTDIR/.zshrc`, or\n `~/.zshrc` when `ZDOTDIR` is unset.\n- Fish uses `~/.config/fish/completions/cotal.fish`, which fish loads automatically.\n- PowerShell on Linux or macOS asks the installed `pwsh -NoProfile` for its current-user\n all-hosts profile and wires an owned `completion.ps1` stub into that file. A missing shell,\n invalid profile reply or missing profile directory refuses without guessing a path.\n\nAn absolute `XDG_CONFIG_HOME` replaces `~/.config` for the owned stubs; a relative value is\nignored. Mutation refuses a relative `ZDOTDIR`, missing startup directory, symlink, untrusted\nowner or unsafe file access. A startup file its owner made read-only is never replaced. `install`\nstill writes its stub, then fails, exit 5, and names the line to add to that file yourself.\n`uninstall` refuses it, exit 2, and changes nothing until you remove that line. On Windows, PowerShell script generation works but automatic\nprofile editing remains unsupported: the guarded POSIX file writer cannot safely replace\nWindows reparse-point paths. Installing files does not establish that the host runs its\nprofile or permits scripts under its execution policy.\n\nInstallation writes a private `.ownership.json` record beside its stub. Reinstall refreshes only\nan unchanged recorded stub. Uninstall removes only that stub and the unchanged recorded startup\nblock, keeping other startup bytes. Modified or unrecorded material is preserved with a refusal.\nA stub from an earlier release without an ownership record needs manual review; matching\ngenerated text alone is not treated as ownership. An unowned startup reference to a managed stub must be reviewed\nbefore removing the stub. These commands cannot unload functions in a parent shell.\n\nCotal writers use a per-shell lock and recheck file identity and bytes before changing them.\nAn unconfirmed filesystem mutation is reported as unknown and is not retried. A confirmed change\nfollowed by another refusal is partial, with the acknowledged file paths retained. This is not\na filesystem compare-and-swap against an editor that ignores the lock.\n\n`update` refreshes existing unchanged recorded stubs through its current-generation CLI. It never\nadds startup wiring or reinstalls a removed stub. Modified and unrecorded integrations are kept;\nany guarded refresh it cannot complete is reported. `update --check` does not refresh completion.\n\nThe old `completion <shell>` form is a usage error. Custom startup commands need manual migration;\nsee [Upgrading](UPGRADING.md#shell-completion-script-in-0910). No command silently\nrepairs custom wiring. Install and uninstall support one finite JSON document or the plain report.\n\n## feedback\n\n```bash\ncotal feedback \"<summary>\" [--type <type>] [--email <contact>] [--details <text>] [--json|--plain]\ncotal feedback \"<summary>\" --key-file <private-file> [--json|--plain]\n```\n\nThe type is bug, idea, friction, praise or other; severity is low, medium or high. Optional area\nand contact name are included only when supplied. The public intake requires a traceable email,\nfrom `--email`, the existing configured contact or local git email. No environment setup is\nrequired. Do not put credentials, private transcripts or unrelated customer data in feedback.\n\nA protected `--key-file` selects keyed intake without placing a credential on argv. An existing\ninternal intake configuration remains supported. The old raw `--key` input refuses before the\nrequest and does not echo its value. Private key files must meet the shared file-ownership and\nprotection policy; unsupported environments refuse rather than weakening that policy.\n\n`--url` can select a self-hosted intake. Remote intake requires HTTPS; HTTP is limited to literal\nloopback development hosts. URL credentials and fragments are refused, and redirects are not\nfollowed. One request has a 20-second deadline and is never automatically retried or rerouted to\na different credential/intake. A network or unconfirmed server outcome can mean the report\narrived: inspect the original intake before sending it again.\n\nJSON/plain receipts report the intake origin, HTTP acceptance and a bounded record ID when the\nserver provides one. They do not repeat the submitted summary, details, contact values, key or\nresponse body. Acceptance does not prove that anyone read the report or acted on it. A success\nresponse without an established ID is reported with that limitation, not retried for a new ID.\nRun a self-hosted intake with [`feedback-intake`](#server-daemons).\n\n## telemetry\n\n```bash\ncotal telemetry [--json|--plain]\ncotal telemetry off|minimal|regular [--json|--plain]\n```\n\nThe CLI sends anonymous usage data: one small event per command as it exits, and at the default\n`regular` level one install summary per day. [Telemetry](telemetry.md) lists every field and what\nis never sent. The bare form prints the level in force, what set it (the settings file,\n`COTAL_TELEMETRY`, `DO_NOT_TRACK` or `COTAL_TELEMETRY_DISABLED`), the install id, the intake URL and\nthe settings file. A level argument writes the file; `on` is an alias of `regular`, and writing the\nlevel the file already holds is a `no-op`. A level the environment forces still wins, and the\nreport says so. `cotal telemetry` itself sends nothing.\n\nThe first interactive run at `regular` prints one notice to stderr. `COTAL_TELEMETRY` set to\nanything but a level refuses `USAGE_INVALID`, and a settings file that does not validate refuses\n`SCHEMA_INVALID` on every command until it is fixed or moved away.\n\n## run\n\nOperate durable workflow runs (cotal-lang programs) from the terminal.\n\n```bash\ncotal run start --file <program> [--timeout <dur>] [--local --admit-read <channels> --admit-publish <channels>] [--on <instance>]\ncotal run resume <runId> [--local --file <program>] [--on <instance>]\ncotal run ps [--endpoint <ep>] [--json] [--on <instance>]\ncotal run ps --page --on <instance> [--limit <n>] [--cursor <cursor>] [--endpoint <ep>]\ncotal run journal <runId> [--endpoint <ep>] [--json] [--on <instance>]\ncotal run answer <runId> <stepKey> [--value <json>] [--artifact <ref>] [--endpoint <ep>] [--local --by <who>] [--on <instance>]\ncotal run amend <runId> <stepKey> [--value <json>] [--artifact <ref>] [--endpoint <ep>] [--local --by <who>] [--on <instance>]\ncotal run revoke <runId> --local --by <who> --reason <text> [--endpoint <ep>]\ncotal run migrate <runId> --local --file <program> [--endpoint <ep>]\n```\n\nEvery verb also takes `--space <s>`, `--server <url>` and `--creds <path>` to pick the mesh and the\ncredential.\n\nHosted run verbs accept `--on <manager-instance>` to choose one full manager id\nas `cotal ps` prints it. A managed seat defaults to the manager that launched\nit; an unbound user-auth caller with several managers must choose one. Without\n`--on` its call is refused with exit 2 before any request, and the refusal names\neach live manager.\n`--on` does not apply to local, offline or seat-only run commands, and a failed\npin never falls back to another manager. Rich run reports label an explicit pin\nas the requested manager, and the JSON target carries `requestedManagerInstance`.\nFollow-up commands printed by the CLI retain the same explicit pin and fill\nin a returned run ID only when it matches the native run-token shape. Keep the\npin on later status, answer and resume calls.\n\n`start` hands the program to the mesh's manager, which validates it, mints the run id (the record\nnever takes a caller-supplied one), drives it in its own process, and answers with the id once the\nrun is recorded; a program that does not validate is refused with every problem listed. `resume`\nasks the manager to take an existing run back and continue it from its step journal; the source is\nthe recorded program, so no `--file` is taken. Neither takes `--endpoint`: the manager records\nits runs under its own endpoint, and naming another is refused. `ps` lists the run records and\n`journal` renders one run's durable records; both only inspect. An open pause prints its question.\nA pause settled with an accepted answer prints its value as JSON plus the recorded answerer,\nartifact when present, time, and answer id, then one `amended` line per later amendment, in the\norder the store committed them, so the last is the current position.\nExpired pauses and ordinary steps print no answer line.\n`--json` on `ps` or `journal` prints each row the manager answers with (or `--local` reads) as one\nJSON object per line. A `ps` row carries `runId`, `endpoint`, `state`, `holder`, `epoch`,\n`journalHigh`, `forkedFrom`, `startedAt` and `programHash` (the values the program's `run()`\nreports; `programHash` is absent for a run with no recorded program), and `revoked` or\n`revocationUnreadable` when the marker says so. A `journal` row is an `activation` or a `step`. A\nstep row carries its `step` key, the `effect` kind and its `name`, `state`, `outcome`, the recorded\n`status` and `errorCode` once settled, and `startedAt` and `endedAt` in epoch milliseconds. An open\npause adds its `asks`, its `deadlineAt`, and for a checkpoint the `onExpiry` it was armed with; a\nsettled pause adds its `answer` and its `amendments`, as the text view prints them. A field the\njournal does not record is absent: a checkpoint opened before `onExpiry` was recorded carries none.\nThe run header and errors go to stderr, so stdout carries only rows; an unreadable revocation marker\nprints its reason there and still exits 1. The text view is presentation and is not a stable\nparsing target. `--json` on any other verb is refused.\n`ps --page`, which is not in release 0.88.0 or earlier, asks one manager for a single bounded\ndiagnostic JSON page instead of the full list:\n`--limit` takes 1 to 16 rows (default 8), and `--cursor` continues a scan from a returned\n`nextCursor`. It requires `--on` and refuses `--local`, and a page proves neither absence nor\nownership ([Diagnostic inventory](workflows.md#diagnostic-inventory)).\n`revoke` writes the run's revocation marker from the project folder; `--by` and `--reason` are\nrequired ([workflows](workflows.md)).\n`answer` resolves an open\ncheckpoint through the manager, presenting as the holder that armed it; the manager records the\nanswerer from your credential, so no `--by` is taken there. A settled step refuses a second\n`answer`. `amend` records a changed position on a settled checkpoint or `ask`: it files a new\nanswer beside the accepted one, naming it, and the journal lists it under the step. The pause stays\nsettled and the run keeps the answer it acted on. A step that is still open or settled without an\nanswer refuses an amend. A spawned seat may amend only an answer recorded under its own name. `migrate` runs the migrate check of an\nedited program against a run's journal, from this terminal under a read credential (`--local`\nonly; the manager serves no run-migrate command): it prints whether the migration is admissible,\nevery orphaned step with its verdict and code, and exits 0 on admissible and non-zero on not. It\nwrites nothing: the commit that would file the migration is not reachable yet, and the report\nsays so. `migrate` refuses `--adopt`, `--release` and `--discard-approvals`, since they decide what a\ncommit does and the check files nothing. `--timeout` sets the default\ncheckpoint timeout for a drive (default 1h). `--on <instance>` sends a hosted verb to one manager\ninstance, with the same rules as [`ps`](#managed-seats). On a user-auth mesh where you run more than one\nlive manager, an unpinned hosted verb is refused and `--on` picks one. `--local` takes no `--on`. `--local` drives in this process instead, over one\nconnection per invocation under the run's own credential minted from the project folder's trust\nmaterial, and is the path on a bare broker with no manager or for a run with no recorded program\n(`cotal run resume <runId> --local --file <program>`); `answer --local` and `amend --local` take\n`--by <who>`, and `start --local` requires `--admit-read` and `--admit-publish` (comma-separated\npatterns or `none`), the run's channel ceiling. On a\nuser-auth mesh the host's own manager refuses the family by name, and `--local` has no credential\nthere. A participant's manager started with `cotal supervise` hosts a logged-in user's runs through\nits issuing host: the auth callout issues the user's manager connection, and every `run` verb rides\nthe versioned rail under that issuance.\n[User-auth run start](design/user-auth-run-start.md)\nrecords the path. The guide is [workflows](workflows.md).\n\n## Server daemons\n\nTwo long-lived infra roles ship with the CLI. They are not part of everyday operation; the delivery\ndaemon comes up automatically with `cotal up --detach` in auth mode.\n\n`deliver`, `auth-service` and `feedback-intake` are deprecated entries kept for existing units and\nscripts, as is `supervise`; help and completion omit them. Each accepts its existing flags and\nprints a warning on every run naming its service preset and the release that removes it. The\ndelivery daemon and auth service that `cotal up` starts run the same code through hidden entries\nand print no warning. New setups use `cotal service add <name> --preset <preset>`; see\n[deprecated daemon entries](UPGRADING.md#deprecated-daemon-entries).\n\n```bash\ncotal deliver --space <s> [--server <url>] [--creds <file>] [--root <dir>]\ncotal auth-service --space <s> --server <url> [--port <n>] [--exchange-public-port <n>] [--exchange-public-url <https://\u2026>] [--exchange-trusted-proxy] [--advertised-server <url>] [--agent-provisioning-url <https://\u2026>]\ncotal service add intake --preset feedback-intake --space <s> --root <absolute-root> --keys <private-keys.json> --creds <private-creds>\ncotal service start intake --space <s>\n```\n\n`auth-service` runs a user-auth space's identity plane: the NATS auth callout, the\ncapability-gated local exchange and JWKS, and, when `--exchange-public-port` is set, the closed public\nexchange/discovery face forwarded by an HTTPS reverse proxy. `--exchange-public-url` is the proxy URL\nadvertised to clients; `--exchange-trusted-proxy` opts into last-hop `X-Forwarded-For` attribution.\n`--advertised-server` and `--agent-provisioning-url` set the broker address and agent-provisioning\nendpoint the public bundle advertises, as on [`up`](#up).\n`cotal up --user-auth` starts and supervises the service for you, so you run it directly only to\nrecover one by hand.\n\n`deliver` runs the server-side Plane-3 delivery daemon: the durable backstop and membership/ACL\nauthority. It is auth-mode-only and single-instance (`--shard`/`--shards` accept only `N=1`);\n`--dev-mint` mints a scoped cred for standalone dev, signed by the local account of the space the\ndaemon serves. `--creds` can start a daemon that already looks healthy, but production renewal is\nnot that file alone: the manager and the daemon must address one credential store. On a stock split host with two project roots, a\ndirect `deliver` is not an independent repair; keep the daemon under `cotal up` on the broker\nhost, or inject the same store into both processes ([embedding](embedding.md#supervisor-signing-authority)).\nTyped by hand on the workstation, `deliver` dials the broker recorded for `--space` in the mesh\nregistry (a mismatching `--server` is refused before any dial, and a record for a different\nworkspace root is refused outright); with no record for the space it falls back to the local mesh.\nThe daemon serves the workspace root that `--root <dir>` names, which must hold `.cotal/`, or else\nthe nearest `.cotal/` above its working directory. With neither, it refuses at start and names the\ndirectory it searched from, before it reads a credential or dials a broker.\nSee the [delivery daemon](delivery-daemon.md). `feedback-intake` runs a self-hosted feedback server\n(requires `--keys` and a scoped `--creds`), announcing submissions into a space channel; flags\ninclude `--host`/`--port`, `--store`, `--space`/`--channel`, `--max-bytes`, and `--rate-limit`.\n`--port` takes a decimal port from 1 to 65535 and is checked before the intake dials the broker.\n\n## Plumbing\n\n`cotal __complete <words\u2026>` is the internal entry the shell-completion stubs call to emit candidates\nfor the current command line; you never run it directly. `cotal agent-bearer` is machine-facing\nplumbing on user-auth meshes: spawned agents exec it to print a fresh short-lived bearer from their\nspawn-time secret; you never run it directly either. Its local arm uses `--dir` to discover the\ncapability-gated loopback service. A remotely enrolled, already-granted agent instead receives\n`--exchange-url <https://base>` in its launch argv: that arm sends `{owner, actor, actorToken}` to the\npinned public exchange with no local capability, follows no redirects, and refuses every non-HTTPS\nURL because the actor token is the credential in the request body. Because a seat execs it on every\nbearer refresh, it skips the connector-seed boot gate entirely: it reads one 0600 token file,\nexchanges it and prints the bearer without consulting or writing the operator-global seed store, so\na newer store generation cannot refuse a live seat's refresh. For the same reason the seat's command\nnames the CLI entry file the `cotal` bin link resolved to at spawn, not the link: an install that\nlater replaces the link, such as the get.cotal.ai launcher over an `npm i -g` link, leaves running\nseats able to refresh. `--manager-call` asks for the\ninstance-bound `manager-caller` view; `--manager-instance <id>` selects an explicit live candidate,\nand an owner with more than one live candidate is refused with their ids until it names one.\nThat mode still prints only the raw token and does not update `--health-file`. A spawn runs it once as the agent auth\npreflight. When it fails there without printing a sentence of its own, the refusal names the cause: the 30 second\ntimeout, the signal that killed it, or its exit code. (`cotal start` is a removed tombstone: it\nerrors and points you to `cotal spawn --detach`.)\n\n`cotal app-identity` is plumbing for a Claude app conversation, and you never run it directly\neither. The Claude connector's local server runs `cotal app-identity provision --name <name>` when\na conversation connects. It picks the mesh as `cotal spawn` does (`--space` selects one) and prints\none JSON line. A new identity opens only after a person approves it at the mesh's authority, so\nprovision needs a signed-in mesh whose authority advertises connection approval v1, and the\nconversation's native `--session`. It keeps the request and its secret in a private record under\n`~/.cotal/app-identities`. Until the person decides, it answers\n`{\"ok\":false,\"pending\":true,\"requestId\":\"<id>\",\"approval\":{\"id\":\"<id>\",\"url\":\"<url>\",\"expiresAt\":\"<time>\"},\"proposed\":<selection>}`,\nwith no credential, secret or path. `proposed` is `null` for a request without a selection.\nProvisioning again sends the same request: it answers the same approval while pending, and the\nlaunch env and the reference that `retire` takes once approved. The grant is what the person\napproved, which can differ from the request. A denial or an expiry drops the record, and the next\nprovision makes a new request. `--selection <json>` proposes channels, a role and capabilities for\nthat conversation. A mesh with static credentials, an open mesh, and a signed-in mesh whose\nauthority this machine holds have no approval page, so provision opens no identity on them.\nThe connector passes `--require-approval`. Every provision needs approval with or without it, and\nan earlier CLI that opened identities without approval refuses the unknown flag before doing\nanything. A saved request that was never issued needs the person's decision even when an earlier\nversion saved it.\n`cotal app-identity retire --dir <dir>` still removes an identity an earlier version opened on a\nstatic mesh: its broker footprint, then its files and directory. When the broker is not answering it\nkeeps them. Each provision also retires those directories whose owning process has exited. For a\nsigned-in identity, `retire` keeps the record until the mesh confirms retirement, and names the\nmesh's phase while it is pending. A request still pending is dropped.\n`cotal app-identity reap` retires the selected mesh's saved conversations whose server process no\nlonger exists. The connector runs it after each signed-in connection, so a connection never waits\nfor that cleanup. Like `agent-bearer`, it skips the connector-seed boot gate.\n\n## watch\n\n`cotal watch` opens the read-only activity screen for the selected space. `w` is its shorthand.\nIt uses the existing observer view authority; a user-auth space requires the corresponding\nadmin view grant. It does not register a participant, bind an inbox or expose local process\ncontrols. Leaving with `q`, Esc or Ctrl-C stops only this observer.\n\nUse `--view events` or `--view messages` to select a feed, or press `f` to change it in the\nterminal. Arrow keys scroll; `g` returns to the latest observations. The feed retains at most\n300 entries. A reported turn ending is not task completion. An interrupted feed keeps its\nlast observations and reports the gap instead of showing them as current.\n\n`--plain` or a noninteractive invocation uses the attributed line stream. Interactive filters\nare not supported by that stream. Authenticated observer views keep direct messages out of\nthe channel tap. Opening this view never launches, stops or attaches an agent. If the reader\nof the stream closes it, as `| head` does, the observer stops the same way.\n\n### Named agent status\n\n`cotal status <agent>` reads the authorized manager census and separates Identity,\nExecution, Mesh and Last reported activity. JSON has `identity`, `execution`, `mesh`\nand `activity` fields; missing optional observations are null with source annotations.\n`--on <manager-instance>` restricts the read. It takes no `--all` or `--components`.\nA requested model is not provider-confirmed. Connector-reported provider\ninformation is labeled exact or name-matched under the same proof. A new manager\nrow marks a roster observation exact only when its principal and lifecycle UID\nmatch the managed seat and its presence view is current. Older or mismatched rows leave exact presence\nand activity null and label any name-matched values separately. A verified\ncondition and its reported start appear with activity; an unverified condition\nstays in a separate name-match field. A waiting observation is not a measured\nblocked duration, and a work event is not task completion. Attach and stop\nhints retain the answering manager's instance when it is a valid native id.\n\nA same-name match on several managers refuses as ambiguous. An unanswered manager leaves\nthe search partial; no matching row in that partial read is not proof the agent is gone.\nThe command starts, stops and attaches nothing.\n\n## Manager maintenance\n\n`cotal manager`, also `cotal mgr`, shows its two scoped operations. The group and its operations\nsupport `--json` and `--plain` through the shared reports. Neither operation installs connectors\nor starts a manager.\n\n`manager seats` lists retained local PTY custody records. `--drain --dry-run` reads the census\nwithout submitting a drain. A real drain reviews the local store and defaults to cancellation;\nscripts require `--drain --yes`. Its scope is all safely childless records in that store when\nthe native pass runs, not a frozen list from the review. Each native reap rechecks process\ncreation identity. Live children stay. An unresolved preflight blocks draining; a partial read\nreturns exit 4. An unconfirmed effect during a submitted drain returns exit 3 and keeps confirmed\nremovals in the result. Raw native diagnostic bodies are omitted.\n\n`manager reconcile` uses the existing guarded issuance-gate repair from the owning local mesh\nroot. It resolves the registered broker without an alternate-target fallback and preserves its\nTLS requirement across repair, liveness and eviction connections. It requires that root's auth\nmaterial and verified holder liveness. Review defaults to cancellation; scripts require `--yes`.\n`--dry-run` checks only the local coordinate and issues no credential or native guard query.\n\nA pre-effect native guard refusal returns exit 2. Its error names the guard's condition and keeps\nthe guard's own sentence as its native cause, which names the freeze-holder and the next step.\nA failure after the repair may have submitted\nwork returns exit 3, without replaying mutations or hiding possible revocation/eviction progress.\nRe-observe that gate and its operation-bound durable cursor before another explicit repair.\nThe final receipt reports the native operation and before/after coordinates, not a running-manager\nverdict. A reopened gate with retained cursor cleanup returns exit 4.\n\n\n### Workflow command forms\n\n`cotal run` (or `cotal r`) lists the authorized workflow records. `run show <run-id>` reads\nrecorded state, open requests and the last 20 journal rows. Its JSON result contains the full\nreturned status and journal. Revoked admission and unreadable revocation markers remain distinct\nfrom the record's state. Neither a record nor a receipt is a live agent-progress report.\n\n```bash\ncotal r\ncotal r start ./workflow.cotal.js\ncotal r show <run-id> --json\ncotal r resume <run-id>\ncotal r answer <run-id> '<step-key>' --value '<json>'\ncotal r amend <run-id> '<step-key>' --value '<json>'\ncotal r check <run-id> ./edited.cotal.js\ncotal r revoke <run-id> --local --by <actor> --reason '<text>'\n```\n\nStart takes a positional source file. Resume uses the host's recorded program. Hosted answers\nand amendments are attributed to the authenticated caller, never a supplied name. Check reads\nthe recorded journal through the existing local check; it commits no migration. Revocation keeps\nthe existing local trust and explicit actor/reason requirements and does not cancel a task.\nHosted start, resume, answer and amend have JSON and plain receipts. The local drive and local\ncheck retain their native output rather than claiming an unsupported structured format.\n\n```bash\ncotal r validate ./workflow.cotal.js\ncotal r plan ./workflow.cotal.js\ncotal r simulate ./workflow.cotal.js --handler ./results.json\n```\n\nThese three forms are offline and reject mesh or caller selection. Validation checks the\nlanguage. Plan lists validated static call sites in source order; it does not evaluate branches,\nloop counts or an execution schedule. Simulation uses the native simulator with a JSON results\nscript. It executes no JavaScript handler and invents no unscripted outcome. A simulation does\nnot establish live authority, installed applications or readiness. All three support JSON and\nplain output and contact no broker.\n\nInside a manager-launched static or open seat, `run turns` reads that seat's assigned turns and\n`run yield <turn-id> --status <done|blocked|handoff>` reports an outcome. Handoff also requires\n`--to <agent>`; `--note <text>` carries context. The native launcher supplies the caller context.\nThese forms verify the seat's own credential and manager, never substitute an operator caller,\nand refuse user-auth self impersonation. They skip operator catalog and installation repair.\nYielding one turn does not complete the entire workflow.\n\n\n### Update availability\n\n`cotal update --check` queries the latest registry version and reads recorded extension versions.\nIt performs no install, reconcile or process restart and takes no mesh selection. It supports\n`--json/-j` and `--plain`. The registry read runs npm with a temporary cache that is removed\nafterwards, no log files and no npm update notifier, so it leaves nothing in npm's cache, log\ndirectory or notifier stamp. Missing registry data or an unreadable extension inventory remains\nunknown or partial, never an empty successful check.\n\nThe report compares first-party recorded versions with the running CLI. It does not inspect\npackage contents, signatures or loaded process generations. `cotal update` and `update --self`\nkeep the existing guarded update, continuity checks, download verification and current-generation\nreconciliation. Their installer progress is native output, not a structured check receipt.\n\n\n### Account screens\n\nBare `login` chooses a known provider from cached account metadata or registered spaces. When\nnone is known, an interactive terminal asks for the provider auth URL. `--idp` selects it directly.\nThe device-approval link and code are written only to stderr. Session tokens are never printed.\nThe sign-in receipt separates account identity from each space's grants. This client's device\nflow requires approval and refuses `--no-input` before requesting a device session.\n\nBare `logout` chooses among cached providers. A single cached provider is unambiguous; scripts\nwith multiple providers must select one with `--idp`. `logout --json` and `--plain` report the\nsame result. No cached session is a local no-op, not proof that all provider sessions are gone.\nRevocation must return before local cleanup. If revocation is unconfirmed the login is retained\nfor retry; a later cleanup failure is partial and keeps the confirmed server outcome.\n\n\n`endpoints --json` and `endpoints --plain` show the authorized live presence snapshot, including\nagents and infrastructure. This legacy live read is labeled as presence, not a saved-reference\ninventory or proof that a service registration is active. Native contract resolution remains a\nseparate read.\n\n\n### Managed launch view\n\n`spawn [<persona>]` now uses the manager and attaches after its native readiness result. The\nattachment is pinned to the attributed accepting manager and the allocated identity/incarnation,\nnot a second display-name guess. `--detach/-d` returns a finite JSON/plain-capable receipt after\nthe same readiness result. The terminal receipt shows the full lifecycle, original request, accepted goal where reported,\nrole and model request separately from any provider acceptance claim. Its Next section names the actual agent, space and accepting manager\nin attach/status commands when their tokens are safe shell words; it otherwise retains\nplaceholders beside the full identity fields. Without usable input/output terminals, default attachment refuses\nbefore allocation and names detached or direct execution. A shell tool's inherited parent PTY\nis not enough.\n\n`--harness/-H`, `--model/-m`, `--name/-n`, `--prompt/-p` and `--cwd/-C` retain their declared\nmeanings. The legacy `--agent` spelling remains accepted; using both harness spellings refuses.\nA `--file <persona-file>` launch uses this route too unless `--foreground` is given. A manifest is\ndeployed with `cotal team deploy <file>`, not `spawn`.\n\n`--foreground` runs the native application directly. It does not support a structured receipt,\nmanager pin or manager control credential. Native exit or terminal loss may end that process;\nno detachable-view or reboot-survival claim applies. Protected internal handoff/enrollment\nchildren keep their existing direct execution and native authorization checks.\n\nIf the agent joins but attachment fails, the result is partial with its original identity and\nmanager. No second launch occurs. A view exit sends no stop request. This route does not offer\nno-manager startup choices or imperative planning.\n\n\n### Observer stream\n\n`watch --json` emits versioned watch NDJSON records from the same MeshView as the terminal.\nActivity, events and messages filters use the shared classifier. Metadata snapshots identify\nscope, reported roster state, native channel counts and membership-read failures. Feed rows keep\nnative message IDs and timestamps; coalesced updates are identified as observed rows, not new\nmessage publications or proof of task completion. No global sequence is invented.\n\nThe feed has a 300-entry window and bounded per-channel prefill, not a complete history export.\nWindow loss, connection transitions and text beyond the 16,384-code-unit row bound produce gap\nrecords. Identity fields are not clipped. The stream does not claim that arbitrary publisher\ncontent is secret-redacted. Leaving the observer stops its own connection and sends no agent\ncontrol request. Connection or authorization failure is an error, not an empty conversation.\n\n\n### Recorded team views\n\n`team` (`t`) reads fresh launch summaries, additive ownership ledgers and their\nroot-local attempt records at the selected local root. When native `up -f` returns from its broker start, it writes a creation-only fresh\nsummary with its full run ID, initial broker PID, declared agent/channel names\nand the initial control-plane, auth-service and delivery-responder results.\nOlder summaries without a broker PID, auth-mode pin or responder observation\nremain readable; `team show` labels their absent facts not recorded and\nreports partial. A PID\nor a responder bound when boot returned is not current process or listener\nownership proof. A private create-only fresh boot attempt is written and fsynced\nbefore the launch spec and broker start. A returned summary is also published create-only and\nfsynced; an intent without a matching auth-mode-pinned summary showing a\nreturned control plane, auth service and (for auth mode) bound delivery\nresponder stays unsettled and\nblocks a later fresh boot. `team show <id>` and the team inventory expose the original intent without\nclaiming whether its broker started. `team show` displays up to 100 declared names from a\nsummary and retains the complete list in JSON; none are claimed to have joined. A missing\nor refused summary after boot is not a reason to boot again.\nThe inventory stays partial because older fresh boots may have no summary. The\nterminal lists up to 100 records; JSON retains all readable records. Its Next column\nsays inspect the stack for a fresh record or review the native scoped teardown plan for an\nadditive ledger; neither row grants permission to stop anything. A fresh summary is\nnot an additive ownership ledger or evidence that an agent joined, and it does not establish\ncurrent process health. `team show <id>` identifies the recorded root and planned whole-stack\n`cotal down --space <recorded-space>` route for a fresh summary. The existing bare `down` does not use\nthe whole-set impact controller. This summary is not authority to run it.\n\nFor an additive record, `team show` prints up to 100 full recorded agent identities\nand lifecycle UIDs where the ledger has them, with the full validated ledger in JSON,\nwithout claiming those agents are still live. Its follow-up uses\n`down -f <file> --run <id> --dry-run` from the recorded root. The file is a required argument,\nbut the full run ID selects the ledger; an edited manifest is not new teardown authority. Review\nthe native plan before a separate apply. Listing ownership does not confer permission to stop\nanything. One guarded `team deploy <file>` route accepts a\nchannel-only manifest whose declared cards are all absent and explicitly set\n`deliveryClass: live`. It refuses agents, runtime requests, defaults,\nbroker bind changes, access edges, existing cards,\nre-apply, registered broker lists and unresolved prior attempts before an effect.\nIt selects the exact registered root, single broker address, auth mode and TLS\nrequirement, then holds the local maintenance lock. The original user bearer and its deployer and writer\nviews must name the same owner and actor. A refused view stops before the\nintent; no static or broader caller is substituted. A scoped writer uses one\nnon-reconnecting broker connection and KV create-only on the existing bucket,\nnever the legacy merge-and-put path. The private intent is published and synced\nbefore any create. Every channel has a synced submitted step before the native\nrequest and a synced accepted revision after its returned acknowledgement.\nThe returned receipt matches every declared channel and both steps. A missing\nacknowledgement or receipt leaves the attempt unsettled; no create is retried.\nIf closing the writer connection fails after the receipt is confirmed, the result\nkeeps the confirmed revisions and warns about the close. A returned receipt\nproves only historical created revisions, not the current\ncards, readers or removal authority. `team show <id>` presents one row per\nreturned channel; incomplete attempts show their submitted/accepted steps.\nThe JSON result retains both forms of evidence. Team inventory and status\nshow returned versus unsettled state. Additive broker addresses in reports\nomit URL user info, queries and fragments; the protected intent keeps the\nexact connection target. Invalid or orphan steps stay visible and\nblocking. Guarded channel-only card removal uses the returned create revisions as described below.\nCompletion reads those receipts locally; it does not query the broker or guess ownership from an\nedited manifest. Channel apply emits one finite rich, JSON or plain result.\n\nThe agent route accepts one to 100 all-new agents on a registered static-auth\nor open mesh, optionally with up to 100 new channel cards that explicitly use\nlive delivery. Defaults, bind changes and stale waivers refuse. User-auth agent\napply refuses before inline persistence. The route selects a manager through an attributed status read,\nthen pins credential issuance and every subsequent request to that full instance.\nIt requires native execution and a matching runtime; it never starts a replacement\nmanager. `--runtime` overrides the manifest's requested runtime but does not\ninstall or switch a provider.\n\nA mixed deployment also requires every requested application to appear available\nin that manager's connector inventory. This is a startup observation, not proof\nthat a later launch will succeed. A separate deployer reader checks the requested\nchannel keys, and a separately scoped writer uses one non-reconnecting connection\nfor native KV create-only. Manager credentials are not used as channel authority.\nExisting cards refuse; no patch, adoption or legacy merge-and-put occurs.\n\nOne private intent records all channels, ordered resolved agent hashes and the\nmanager before effects. Channels are created first, with submitted and accepted\nrevision steps synced before continuing. Each agent's original request is synced\nbefore submission, its native allocation before goal following, and its matched\njoined result before the next agent. A joined result must match the accepted\nname, principal, lifecycle, harness and runtime. Unreadable or unknown phases\nremain unsettled and are not replayed. The full receipt is saved only when both\nchannel and agent sequences match the original intent.\n\nA later failure retains earlier channel creates, allocations and joins without\nrollback. Writer-close uncertainty is reported separately from returned native\nreceipts. The finite JSON/plain/rich report distinguishes those receipts from\ncurrent health and task completion. `team show` exposes both native journals,\nincluding partial channel creates when a later launch did not complete. Inventory,\nstatus, down and reset distinguish returned mixed deployments from unsettled\nattempts. Retained verification and guarded removal use the paths below.\n\n`team boot <file> --dry-run` shows the resolved fresh topology, its target root,\nrequested applications, roles, models and channels with declared delivery,\nreplay and window settings. Absent channel settings are not inferred from a\nsaved registry that this preview does not read. An open manifest with an\nexplicit durable default or channel declaration is warned as unsupported:\nits native fresh apply refuses before effects because open mode starts no\ndelivery daemon. Direct `up -f --dry-run` refuses the same contradiction.\nIt reads the local control\ndirectory, recorded spaces, maintenance journal, the selected manager PID record,\npossibly live process records, the root's additive deployment ledgers and\nattempts, and fresh boot attempts/summaries, plus selected-space service\ndefinitions, instances, supervisor receipts and retained native attempts. An\nunreadable additive or fresh record is listed rather than treated as absent.\nThe team inventory separately lists unsettled fresh and additive attempts\nand returned agent/channel receipts. `team show <id>` distinguishes those\nhistorical records without inferring current card or broker state. Bare\n`down --dry-run`, `team deploy --dry-run` and both reset previews expose the\nrelevant retained additive IDs and unreadable paths. The legacy bare `down`,\n`down --preserve-state`, fresh `team boot` and `reset store` refuse unsettled\nintents and unremoved agent/channel receipts before effects. A complete matched\nstatic agent-retirement receipt releases only its original deployment's blocker.\nThis is a known-blocker guard,\nnot complete broker ownership. The service records belong to the workstation's selected-space store,\nnot necessarily this boot root; their recorded\nstate does not establish live ownership. A dead manager record remains visible,\nnot proof that a later boot may replace it. For a concrete candidate broker\naddress, the preview also lists other local registrations with that exact\naddress. A match blocks fresh apply; no match does not prove exclusivity against\naliases or remote tenants. A host-derived candidate leaves this comparison not\nchecked.\nWhere the manifest supplies a broker URL or uses the default without a host override,\nit makes a credless reachability probe. A failed probe is not proof of absence on a\nTLS-first listener. The result is partial: port binding, exclusive local ownership,\ninstalled providers, credentials, grants and startup readiness have not been checked.\nIt writes no record and starts nothing.\n\n`team boot <file>` requires a prepared local root and starts its environment through the service\ncontroller. A root without a stack journal takes the fresh static-auth or open route. An eligible\nstopped stack can resume in static-auth, open or user-auth mode. The manifest's channels seed the broker. The stack's own manager\nstarts its agents from a launch spec pinned by digest. One rich, JSON or plain receipt includes\nthe controller members and requested topology. Agent joins are not checked, so the receipt is\npartial. The fresh attempt and summary are launch records, not stop authority. No additive\nownership ledger is created; `cotal down` uses the stack journal and each member's native lifecycle.\n\nBefore writes it checks the selected root, auth mode, broker address, runtime, connectors and\nretained ownership records. A journal, legacy PID record, possibly live component, service instance,\nsupervisor receipt, native attempt, additive ledger or unsettled fresh attempt blocks a fresh boot.\nUnreadable records are never treated as absent. The lock protects local preparation, and ownership\nand bind checks run again before startup. A bind probe releases its socket and does not reserve\nthe address. A later failure keeps its attempt and any started members, reports the known partial\nor unknown outcome and never repeats effects. A failed summary write also keeps the running stack.\n\n`team boot --foreground` holds the stack's lifetime in the terminal. Ctrl-C or SIGTERM invokes the\nsame guarded whole-stack stop as bare `cotal down`, without force killing members. Terminal loss\nor an external kill is not a confirmed stop. Foreground apply takes neither JSON nor plain output;\na dry-run remains a finite preview. It does not establish native apply readiness or platform support.\n`up -f --foreground` still refuses on its retained launcher path.\n\nA recorded stack resumes only after its last whole-stack stop completed and its manager was never\nattempted. Its broker inputs must be recorded, every applicable member must have its original\nsettled stopped record, and no supervisor, retained custody or earlier team boot may remain. The\nbroker requires its kernel-exit receipt. Auth and delivery use their settled process stop and\nreleased custody; that is not proof of credential, lease or authority retirement. A generic\nprocess stop now records a `process-exit` receipt on the stopped instance when that stop signalled\nthe pinned process and saw it exit. `service stop --json` and `service status --json` expose it as\n`processStop`, including the stop request, original process identity, signal and observation time.\nHuman stop and status reports show the recorded signal, request and exit observation time, or\n`Process stop evidence: not recorded`. This is retained evidence, not a new signal from a repeated\nstop. An instance that was already gone gets `processStop: null`. Resume does not yet require this receipt. The resume receipt\nidentifies the prior stop and each member's evidence. Changed records refuse under the lock.\nA journal without recorded broker inputs cannot take this route. Restart it once with its original\nexisting `--store-dir` and its original `--max-file-store` and `--host`, if set, then run `cotal down`\nbefore the team boot. Omitting those values is safe only when the original start set neither.\nPreparation rechecks that explicit old store's directory identity instead of recreating it if it\ndisappears. These checks do not exclude an unrelated local process changing the path. Open broker\nconfiguration stays inside the selected root's `.cotal` directory, even when its store is elsewhere.\n\nFor a first user-auth team, prepare with `cotal up --user-auth --idp <url>`, then `cotal login`,\nexplicitly grant the invoking user with `cotal auth grant cli --sub <subject>`, and run `cotal down`\nbefore `cotal team boot <file>`. Do not start the manager between preparation and that stop. A\nstatic-auth or open preparation uses `cotal up --no-manager` before `cotal down`. No implicit owner\ngrant or borrowed caller is substituted. A previously attempted manager is never adopted.\nWindows cannot apply, because the stack's process services need Linux machine and boot identity.\n\n`--space` must agree with the manifest. Hand-registered and discovered spaces are not local boot\ntargets. Public broker addresses and fresh-attempt views omit URL user info, queries and fragments;\nthe private intent keeps the target. Native warnings and failures remain on stderr. Installed\nextensions may produce their own output outside the finite result.\n\n`team deploy <file> --dry-run` resolves the manifest and personas and requires a\nregistration for the declared space in this root. It shows the declared\nchannel delivery/replay/window values separately from its non-atomic live\nregistry classification. If the selected running mesh is open, an explicit\ndurable channel declaration is warned as unsupported by its missing delivery\ndaemon; the read-only preview remains available. Additive deploy does not\nseed manifest-wide defaults. For an existing\nunmanaged card, it names only explicitly declared fields that differ from the live value, never the live\ninstructions or description body. No declaration authorizes overwriting a\nsaved card.\nIt makes scoped, TLS-bound\nnative reads of the presence and channel-registry buckets. A complete presence\nscan can prove an empty roster; a failed or malformed scan refuses rather than\nclassifying absence. It classifies channel cards against the registry and agents\nagainst a unique prior ledger for the same manifest content. The derived membership\nfeed, when readable, shows a lower bound of other readers, not authorization.\nThese observations are non-atomic and do not reserve names or prove that a manager,\nruntime, connector, channel writer or launch grant will be ready. An unregistered or\nonly prepared space refuses. `--runtime` changes the requested runtime; `--allow-stale`\nis apply-only and refuses in preview. Apply accepts all-new live channel-only\nmanifests, or bounded all-new agent and mixed manifests on static/open meshes\nthrough one native manager. An identical settled channel-only deployment returns\na no-op only after its recorded target and every original create revision match\nleader-served native reads. It reuses that full deployment ID without issuing a\nchannel writer, creating an intent or changing cards. Missing or changed cards,\nambiguous or unsettled attempts, and removed ownership cannot become this no-op.\nNothing is repaired or adopted. The result names its observation time; it does\nnot establish current readers or health.\nFor an identical settled agent or mixed deployment, reapply verifies the original manager and every\noriginal incarnation. Static-auth uses active live slots; open meshes use the pinned manager's exact\nidentity and lifecycle rows. Mixed cards must retain their original create revisions and content.\nMissing, retired or replaced agents refuse; nothing is relaunched or adopted. Persona drift refuses\nunless `--allow-stale <names>` names the stale agents. Those agents keep their old policy and receive\na warning; the waiver never restarts or updates them. The no-op records an observation time and\nlabels its ordered reads non-atomic. Unsettled, ambiguous or removed ownership cannot become that\nno-op. User-auth agent launch and retained verification remain unsupported.\nRuntime requests must match the original incarnations; no runtime or manager replacement is inferred.\n\n`team remove <full-deployment-id> [--dry-run] [--yes]` can retire a returned\nstatic-auth agent-only deployment with at most 100 original incarnations. It\nrequires the original registered root, broker, TLS mode and exact manager.\nThe revision-32 native `slots` query checks each recorded name, principal and\nlifecycle UID. A successor, omitted slot or unconfirmed transition refuses\nbefore a stop. A dry-run only reads this impact; apply requires a default-cancel\nterminal confirmation or `--yes` in scripts.\n\nUnder the root maintenance lock, apply records one private synced removal intent\nbound to the original creation receipt. Each necessary stop has a synced original\nrequest marker and an expected principal/UID guard. An accepted stop is not\ncompletion. The CLI observes that same incarnation under a 30-second loop budget, in addition\nto each native read's request bound, checking\nits native retired slot, retired lifecycle head and cleanup-complete fact. It\nretains both revisions and the answering manager epoch before continuing. An\nalready retired incarnation needs no repeated stop. A missing row, successor,\ninterruption or observation failure stops the sequence without retrying effects.\n\nOnly a complete matching retirement sequence yields the create-only full removal\nreceipt. Earlier confirmed retirements and stop requests remain in rich/plain/JSON\noutput on a partial or unknown result. An unsettled removal intent stays blocking\nand is never resumed by repeating its effects. `team show` keeps its original creation and removal\njournals, and inventory/status/down/reset distinguish historical retired receipts.\nThe full receipt releases only this deployment's known additive blocker, not any\nother service, broker, shared-root or process guard. It never authorizes stopping\ncurrent alias holders. No channel, broker, service or remote account is removed.\n\n`team remove <full-deployment-id>` also removes the registry cards of a returned channel-only\ndeployment, with `--dry-run` for a read-only preview and `--yes` for scripted apply. It pins the\noriginal receipt, root, broker, auth mode and TLS requirement, and refuses changed card revisions,\nconflicting retained ownership or unreadable records. The preview discloses delivery and replay\nfallback to space defaults and removal of description/instructions. A derived membership feed is\ndisplay-only and never authorizes or blocks this metadata change.\n\nApply records a private intent before any delete. Each card delete is conditioned on its original\ncreate revision, then read back as a later removal marker. Already absent cards name the remover\nunknown. Missing acknowledgements remain unknown with a blocking intent and no retry. A complete\nreceipt releases only that deployment's retained blocker. History, grants and readers are unchanged;\nthe channel stays addressable and no reader exclusivity or complete cleanup is claimed. Repeating a\ncompleted removal returns its historical receipt without deleting a later same-name card. A new\ncard intent records the registry stream's name and creation time.\n\n`team remove <full-deployment-id>` on an unsettled removal intent reconciles it from native\nobservation instead of refusing. `--dry-run` shows each original member's classification, and\n`--yes` records only what the observation proves. It never sends a stop or delete, never repeats\nan unknown action and never continues a member that was not submitted; that member is reported\nunattempted and keeps the deployment blocking.\n\n- A card whose delete was submitted without a confirmed outcome is read with the deployer view\n from the registry stream the intent names, between two stream identity reads. A later removal\n marker, or no message at all, records `observed-removed` with the remover unknown, and it counts\n toward the receipt. A later value records `observed-superseded`; that card is not owned and the\n deployment stays blocking. A card still at its create revision stays unknown, because the\n original conditioned delete could still be applied. An older revision, a revision past the\n stream's last sequence or a recreated stream refuses. A read failure is never treated as absence.\n An intent written before stream binding keeps its submitted cards unknown.\n- An incarnation whose stop was submitted without a confirmed outcome is read with the privileged\n control instrument, which holds no stop or attach rows, from the original manager's exact-name slots. Only the original lifecycle\n UID's retired slot and head with cleanup complete record a retirement, under the original stop\n request. A terminal in progress stays open. A live original, an active slot with no live process,\n a retired slot without cleanup proof and an alias that now names a successor stay unknown. No\n process or presence absence settles anything. Open and user-auth deployments are unsupported.\n- A mixed deployment reconciles retirement first. Its card intent is observed only after a\n complete retirement receipt, and a retirement settled by reconciliation does not start a card\n phase.\n\nA receipt is written only when every original member settles as removed or retired. The\nreceipt then releases that deployment's blocker as an ordinary removal receipt does.\n\n`team remove <full-deployment-id> --dry-run` can also select a validated legacy ledger. It refuses a\nfresh boot ID or a changed root, broker, auth mode or TLS requirement. The old ledger removal path\ncannot adopt a new receipt. For a legacy ledger, a scoped deployer read asks the serving manager for a bounded native PS\nsnapshot and compares recorded IDs, names and lifecycle UIDs. It reads current\nKV revisions for ledger-named channels without exposing their descriptions or\ninstructions; an unavailable registry read refuses rather than treating cards\nas absent. The old ledger has no creation revisions, so a current revision and a\nmatching name do not authorize deletion. It also reads the derived membership\nfeed when available and shows other observed members on owned channels; that feed\nis display-only and cannot authorize channel removal. A missing or unusable manager\nPS refuses rather than calling owned agents absent. On a user-auth mesh a deployer view the\nidentity provider refuses is `PERMISSION_DENIED` (exit 2), with the provider's sentence as the\nremedy. An unavailable membership feed\nstays explicitly unknown. Other retained or unreadable additive attempts under\nthe root are shown separately; their effects are not attributed to this ledger.\nThese native reads are not atomic. The preview changes nothing and cannot\nauthorize a stop. Legacy removal still refuses effects, including with `--yes`. Agent retirement\nremains static-auth only. Channel-only metadata removal uses the existing scoped writer on static\nor user-auth meshes, or the open mesh's bare connection; it grants no new access.\n\nA returned static mixed deployment uses one preview, confirmation and maintenance lock. It first\nretires the original agent incarnations through their native manager. Only a complete retirement\nreceipt permits fresh card checks and deletes conditioned on the original creation revisions.\nBoth receipts are required to release the deployment's blocker. A partial or unknown phase stops\nthere without replaying its effects. An interrupted transition can continue only after complete\nretirement and before any card-removal intent exists. Open and user-auth mixed removal refuse.\n\n`team check <file>` resolves the native manifest and referenced personas without a broker.\nIt lists declared per-channel delivery class, replay and replay window without claiming an\nabsent field's live registry value; the native seed may merge over saved metadata.\n`--graph` shows effective channel access edges from merged agent policies, marking persona-inherited\nchannels that have no manifest card. It also shows each agent's role, capabilities and whether\ncapabilities came from the manifest, a persona or neither. Terminal tables show up to 100\nrows of each kind. The JSON result includes all resolved sources, merged policies,\ninherited scopes and, when requested, the channel graph, without persona bodies,\nkickoff prompts or opaque launch-option values.\nThis file check does not prove live authorization, installed applications or launch readiness.\nThese read views do not apply a boot, an additive deployment or a removal.\n\n\n### Terminal input receipts\n\n`input <agent> <text>` writes through the selected manager's native input command.\nUse `--text` when the value begins with a dash; do not combine duplicate positional and flag\nsources. `--no-enter` omits the trailing Enter request. JSON and plain receipts identify the\nanswering manager and its reported byte count without repeating the input body. An acknowledgment\nis not proof of application consumption or task completion. Unknown results are not retried.\nManaged-launch failures retain native request and accepted-goal identifiers when a reply supplied\nthem, including when following that accepted goal failed. No identifier is invented for a silent\nsubmission, and a missing effect outcome is not a proved pre-execution refusal.\n\nGoal observation keeps the caller's authorized route. A class caller reads its original goal on\nthat class route; an explicit `--on` or credential-bound instance stays pinned. Learning the\naccepting instance ID does not confer an instance route or cause another launch. The native\nfollower verifies the returned fact against the original caller and goal. A launch the manager\nrefuses before its first effect, such as a missing persona file or connector, carries\n`not-executed`. The CLI reports it `refused` with exit 2 and prints the manager's reason, which\nnames the file it looked for. A failure after the manager reserved the name or wrote a launch file\nkeeps its native outcome, and the CLI never reclassifies it from its message.\n\n\n### Stopping an agent\n\n`stop <agent>` requests graceful native despawn. `--force` requests the existing\n`graceful:false` contract; it does not grant authority or bypass incarnation checks.\n`stop --self` uses only a verified managed seat caller and its attributed manager. It accepts no\nother target or credential. Missing, mismatched and unsupported user-auth seat context refuses\nwithout switching to the operator. JSON and plain modes show the acknowledged native result,\nnot a separate measurement of process exit. Stop and input do not seed connector installations.\n\nFailed name resolution refuses before a stop, terminal input or first attach is submitted. The\nreceipt retains the inspect error and responding manager. It records local pre-submission proof\nwithout inventing a native effect outcome.\n\nWhen the manager refuses a stop, terminal input or first attach before anything ran, for example\nbecause the agent is not running or the caller may not control it, its reply carries\n`not-executed`. The CLI reports `refused` with exit 2, names the agent and prints the manager's\nreason on one line. A failure after an effect may have started, such as a terminal write the\nruntime rejected, stays `unknown` with exit 3, still shows the manager's reason, and is not retried.\n\nManaged imperative spawn accepts `--restarts <n> --restart-window <ms>` as a pair, using the\nexisting native `supervise` object. Both values must be positive safe integers. Direct foreground\nlaunch and the legacy manifest form reject these flags instead of ignoring them.\n\n\n### Native endpoint reports\n\n`describe <class>` shows the authorized command table, capabilities, target modes and answering\ninstance/epoch. JSON includes the native compiled schemas and verified closure digests. This is\nnative discovery, not a saved-reference inventory or a grant of authority.\n\n`invoke <class> <command> --args '<json>'` validates public inputs before target preparation,\nthen uses the native fetched contract before publication. `--on` pins one full instance ID; a\ncredential-bound pin must agree. Request IDs and actual replying instance/epoch remain in the\nresult. Refusals with a proved `not-executed` outcome stay distinct from unknown mutations.\nThere is no blind retry of an unknown operation.\n\nPlain and rich reports show at most 60 whole result lines; `--json` preserves the full returned\nvalue. Endpoint-provided result content is not claimed to be secret-redacted. A successful action\nacceptance is not whole-task completion. This route does not use saved references or cover the\ncomplete endpoint namespace.\n\n\n### Watch views\n\n`watch --view` accepts `feed`, `events`, `messages`, `roster`, `channels`, `waiting`, `dms` and\n`topology`; `activity` remains the feed spelling. The terminal cycles available views with `f`.\nRoster and waiting rows show reported state without inventing blocked duration. Channel counts\nkeep retained messages separate from observed arrivals. Topology displays broker-sourced live\npatterns and durable channels, not task dependencies or permission grants. Missing or unreadable\nmembership is explicit, never an empty complete graph. Long identities wrap without clipping.\n\nThe authenticated observer remains narrowed to channel traffic. Its DM view refuses instead of\nbroadening credentials or inventing a private conversation identity. Open-space DM observations\nretain native sender and target IDs. Use `--json` for versioned view records or `--plain` for line-oriented reports outside a\nterminal. Both use the same bounded observer lifecycle and shared view model.\n\n\n### Authority\n\nBare `auth` reports only safe cached provider metadata. It does not verify a session online,\nturn a sign-in into a grant, or print tokens. Its next steps name the `--space` you gave or the\nspace a bare command resolves; with no space they point to `cotal space`. `auth grants` reads the\nselected root's authoritative user-auth actor ledger; a root without its owner secret and pinned IdP\nrefuses rather than reporting an empty authoritative list. A static or open space has no such ledger\non any machine, so there `auth grants`, `auth grant` and `auth revoke` refuse with `UNSUPPORTED` and\nsay that actor grants apply only to a space started with `cotal up --user-auth`. A space that cannot\nbe resolved refuses with its next step, such as `cotal up` or `cotal space`.\n\n`auth grant <actor>` and `auth revoke <actor>` use the existing local operator authority path.\nChoose `--sub` or `--owner`, never both. Grant requires explicit scope/read/post lists unless\n`--full` deliberately selects the native wide defaults. These usage errors refuse with exit 1 before\nthe space or its authority is read, and one refusal names every missing input. Re-grant/revoke\nretain predecessor lifecycle retirement and concurrent-row checks before replacing or deleting\nauthority. A retirement that did not run, because the auth service is not running, refused the\nconnection or answered that the retirement did not run, is refused (exit 2) with the actor row\nunchanged and the recovery named, usually `cotal up`. Any other failed retirement request may have\nstarted the retirement, so it is unknown (exit 3) and is not retried. Grant\nrows in reports contain scope and ACL metadata, not token hashes or secret material.\n\nRevocation receipts separate committed deny-new from the delivery daemon's live-eviction notice.\nThey do not claim all related processes stopped. An interrupted mutation is unknown and is not\nretried. Neither grant management nor actor reads install connector packages. Grant management\ndoes not issue credentials, export signers, retire lifecycles or act on capability handles.\n\n\n### Saved endpoint references\n\n`endpoint` (`ep`) reads local references only and labels live state Not checked. `show <name>`\nshows the saved space, native class, full instance pin and storage revision. These commands do\nnot connect to a broker or infer registrations from participant presence.\n\n`endpoint add <name> --file <reference.json>` imports a strict version-1 reference object.\nGuided add is available in an interactive terminal. Names use case-safe hexadecimal\nstorage keys under the workstation endpoint directory. Imports are capped at 256 KiB. References\nreject unknown fields, raw inline credentials, target-space mismatches and malformed native pins.\nA credential reference is an owned private regular file; its contents are not read when saving.\nExisting names are not silently overwritten.\n\n`endpoint remove <name>` defaults to cancel, or takes `--yes` for scripted confirmation. It\ncompares the held revision and content under the private writer lock before forgetting metadata.\nIt never deregisters a native endpoint, stops a process, removes credential files or revokes grants.\n\nProtected no-follow/nonblocking file access supports POSIX hosts. Windows uses its access-list\nmodel for local records instead: a credential reference there must be a regular file, and its\nowner, mode and links are not checked. Saved references do not provide native live\ninventory, invocation resolution or the remaining endpoint verbs.\n\n\nSaved-reference reads validate record structure without opening or revalidating its credential\nfile. A moved credential does not prevent inspecting or forgetting the local mapping. Protection\nis checked when importing/saving a credential reference and must be checked again before any\nfuture invocation uses it. Read receipts label credential validation Not checked.\n\n### Local service definitions\n\n`service` (`svc`, `services`) reads the selected space's local definitions and existing component\nPID observations. Saved definitions, process presence, ownership and serving readiness are separate\nfacts. The definition table counts its already-read instance records and labels any sole instance's\nrecorded supervisor and historical readiness. The separate instance table gives full IDs,\nprocess observations and each recorded supervisor; a record count is not a count of live services.\nRetained native install, uninstall and unit-action attempts appear with their full instance ID,\nstep, position and whether an instance record still exists. They mark the inventory partial:\na submitted or unknown step may have changed the original unit even if its instance record\nis absent. A legacy PID record is not adopted into the new instance controller. Unreadable\ncomponent records make the inventory partial. Reads do not start, repair or install a service.\n\n`service add [name] --file <definition.json>` imports the version-1 service object described under\n[Adding a service](#adding-a-service). A declared preset uses\n`service add <name> --preset <preset> <declared-options>`. Guided input offers installed presets\nand file import. Input is capped at 256 KiB, checked against the selected native space and its\nschema before a private record is saved. Whether a root or credential suits the preset is checked\nat start. A process definition needs an existing executable and\nabsolute working directory. Credential file protection is checked without reading its contents.\nAdapter definitions use an installed local-process provider's versioned, closed option schema.\nAdding or previewing a definition validates cached metadata without importing provider code. The\ncache records SHA-256 fingerprints of the declaration and installed package metadata. Missing,\nchanged or version-skewed metadata refuses with an update or re-add remedy. These fingerprints\nare not publisher signatures or a proof of every executable file. No remote schema is fetched.\n\n`service show <name>` reads declared policy, not live state. Public reports omit argument bodies,\nadapter option values and credential contents. Missing launch paths do not prevent reading the\nsaved declaration. Records live under the workstation store's case-safe per-space service path.\n\n`service remove <name> --yes` forgets only the unchanged definition. Interactive removal defaults\nto keeping it. Unknown or foreign instance custody makes removal refuse. The process driver verifies that every\nowned direct incarnation is gone and that no OS supervisor is installed before removing its\nunchanged instance records and definition. Logs are retained. No process or OS unit is removed\nby this operation. The presets are described below. A preset is not installed under an OS\nsupervisor.\nOn Windows, local records use the access-list model described under `up`. There, `cotal ext add`\nand the setup seed record a service receipt for an extension that declares service adapters or\npresets, and starting one of its process services refuses Unsupported, because the process driver\nneeds Linux machine and boot identity.\n\n### Service presets\n\nBare `service` lists declarative presets separately from definitions and process observations.\nUse `service add <name> --preset <preset> --help` for that preset's option flags. Parsing, scoped\nhelp and completion derive those flags from the adapter's closed schema. Boolean options have\npositive and negative forms; a pair refuses. A boolean whose schema admits only true, such as an\nexplicit intent, has only the positive form. Numeric and complex options use JSON syntax.\nA preset no registered or installed provider declares refuses by name; for `web` the refusal\nnames `cotal ext add @cotal-ai/web`.\nOptions that collide with service/global flags require the file form. No preset is installed or\nprovider code imported by help, completion, a guided question or definition creation.\n\nA provider registers a `service-preset` alongside its `local-process` adapter. Preset metadata\ncontains its name, adapter, summary and readiness deadline. It does not duplicate option schemas,\ncontain executable factories or supply credentials. Cached preset and adapter declarations share\none package-metadata fingerprint receipt. A changed selected declaration refuses during dispatch.\nPath annotations require absolute physical files/directories, or owned private files, without\nreading their contents during definition validation.\n\nThe CLI's own presets are `auth-service`, `broker`, `delivery`, `exec-endpoint`, `feedback-intake`\nand `manager`. The `linear` and `web` presets come with their extensions. The intake preset is\n`feedback-intake`. Supply its `--root`, protected `--keys` and scoped\nbroker `--creds` paths in the selected registered space. Optional options include `--server`,\n`--host`, `--port`, `--channel`, `--store`, `--max-bytes` and `--rate-limit`. A supplied store must\nalready be private. Without one, the intake appends to a private instance-keyed store beside its\nservice records. Feedback records and logs remain private local data and are retained on removal.\n\nThe `broker` preset uses `nats-broker`. It requires a prepared local `root`, a protected existing\n`config` inside that root's `.cotal`, an absolute `binary` and one explicit `server` address. It\nprepares no trust, generates no configuration and starts no other service. Legacy broker custody,\nknown shared registrations, maintenance and mismatched transport refuse. Its creation-pinned\ndirect child writes a native ports report in a fresh private launch directory, and the readiness\ncheck sets that report to `0600` before it reads it. Its listener must\nmatch the declared IP, port and transport. DNS, wildcard and mapped-IPv6 target declarations refuse\nbefore launch. This reports the child's client listener, not JetStream readiness, endpoint serving\nor a fresh trust check.\n\nBroker stop has a separate kernel-exit policy, with no invented endpoint principal or epoch.\nKnown live or unresolved dependants refuse. After confirmation the controller rechecks the scope\nand process, sends one SIGTERM and waits for that incarnation to exit. Its receipt distinguishes\nprocess exit and removal of the ports file from a native shutdown acknowledgement or cleanup.\nAn unconfirmed stop keeps its original request; no force kill, replay or restart follows. A typed\nadapter refusal before child launch keeps its boundary code, safe cause and remedy. Its local\ninstance receipt remains diagnostic history, recorded as failed with no process, so a later start,\nstop, removal or `down` is not blocked by it. Named multi-service stops do not use this policy. Local registry absence does not prove that\nno remote client or tenant uses this broker, and whole-stack sharing is not checked.\n\nCreating the intake definition launches nothing. Starting it does not start a broker, manager, delivery\ndaemon or installer, mint a credential, or substitute an operator caller. The native child\nreuses the existing HTTP/mesh intake implementation and preserves the target's TLS requirement.\nIt publishes a create-only, full-instance/full-request startup receipt only after the HTTP listener\nand native signal handlers exist. Adapter readiness matches that receipt to the creation-pinned\nchild. It reports an owned running intake, not native endpoint serving, developer action or a\npromise that a future request will succeed. An old receipt is never a replacement launch.\nThe top-level `feedback-intake` entry is deprecated: it keeps its flags for existing callers and\nwarns on every run.\n\n`delivery` uses the selected registered broker root and an existing protected delivery credential\nfrom that root's native store. Supply `--root` and `--creds`; `--server` may repeat the recorded\nbroker but cannot replace it. Open targets refuse. The native daemon still checks its store,\naccount scan and singleton shard lease. No manager, broker or installer is started implicitly.\nA live, foreign or unresolved legacy delivery record refuses rather than being adopted. The\nadapter publishes its request-specific startup receipt after the native runner reports Ready\nand its shutdown handlers exist. That is not a fresh liveness response or proof of endpoint\nserving. The existing `deliver` machine-facing entry stays for the stack callers that still use it.\n\nNative service children retain only the explicit base/path/terminal environment allow-list and\nselected references. `COTAL_HOME` and XDG paths retain the invoking workstation registry. Arbitrary\nambient credentials and agent identity markers are not inherited by generic process services.\nA component needing the invoking caller must supply its supported native handoff, not infer a new\noperator identity. In particular, the web service does not receive the invoking caller's\nauthority.\n\n### Native Linear service\n\nThe `linear` service preset uses an existing native `account`, a reverse-DNS `endpoint` in a\nnamespace you own, and the selected registered static-auth `root`. An optional `server` only\nrepeats its broker. Account login/token setup remains a separate explicit operation. A start whose\naccount has no record refuses with `TARGET_NOT_FOUND` and the `linear account add` route before a\nchild is launched. That check reads the Cotal home of the process that runs `service start`. While\n`COTAL_HOME` is unset, a definition whose `envRefs` set `HOME` (on Windows, `LOCALAPPDATA` or\n`USERPROFILE`) can give the child another home, so that start refuses with `UNSUPPORTED` before a\nchild is launched. Set `COTAL_HOME` for the start, or remove that reference.\n`service add` checks only the declared option types and lengths. The account\nand the reverse-DNS endpoint name are checked when the service starts, and an invalid endpoint\nname refuses that start before a child is launched, leaving a failed instance record. The native\nstatic issuer, inventory, registration, credential renewal, request handling and deregistration\nstay in the Linear implementation. No generic caller authority, new account or broker dependency\nis supplied by the preset. User-auth and open spaces refuse instead of using another issuer.\n\nAn explicit start can allocate another instance through the native registration path. The preset\ndeclares foreground control through the shared terminal-bound lifetime; OS-supervised service\ncontrol is not supported. The child keeps the selected\nroot and registered TLS requirement and does not print upstream bodies or credentials into its\nstartup report. Its private phase records initialized native serve handlers and installed shutdown\nhandlers. It does not claim a broker-flushed subscription, a fresh authenticated serving response\nor upstream business-work completion. Reports label it process/adapter initialization, not\nendpoint-serving proof.\n\nStop retains the native serve close and bounded deregistration policy. Original process exit\nstill does not establish that deregistration or other cleanup completed. The legacy\n`linear serve` foreground entry remains available for installed callers; the independent\nservice route does not call that parser or its printed launch output.\n\n### Native auth service\n\nThe `auth-service` preset requires a registered hosting `root` with its existing prepared\nuser-auth projection. It loads the native callout account, issuer, owner secret and service key\nprojection. Saving or starting this preset does not generate those keys, create an account or\nstart its broker, delivery daemon or manager.\n\nThe preset options derive from the native daemon flags. Ports are bounded integers; public\nexchange options keep the native validation and reverse-proxy requirement. Advertised URLs\ncannot put credentials, query parameters or fragments on the child argv. No environment-export\nonboarding or generic caller-credential substitution is used.\n\nThe preset supports a prepared local user-auth root and its registered plain loopback NATS\nbroker only. The native authority path does not yet carry a required TLS policy through every\nconnection. TLS-required and remote broker targets refuse before authority startup rather than\nweakening that requirement. The preset declares foreground control through the shared\nterminal-bound lifetime; OS-service installation remains unsupported.\n\nThe real child exclusively claims the existing native per-space PID slot and publishes its\ncreation pin privately before opening authority planes. Any existing slot, including a dead or\nmalformed legacy record, refuses without adoption or reclaim. The startup phase is written only\nafter native discovery, flushed callout/listener initialization and shutdown handlers. Discovery's\nloopback capability stays in its native private store; no capability or bootstrap URL appears in\nthe startup phase. This is process/startup evidence, not a fresh authenticated endpoint response.\nA confirmed native close releases only this child's matching PID custody. An unconfirmed close\nkeeps that evidence. Service reports still do not claim lease, registration or family cleanup.\n\n### Manager stop impact\n\nThe native manager status contract includes its selected `lifecycleUid` and a caller-scoped `stopImpact` observation. It lists\nvisible managed principals and lifecycle IDs, their runtime modes and the seat actions a default\nstop or `--with-agents` would request. In-process PTYs without a release hook stop by default;\nreleasable seats lose manager custody. Preservation or pending resume uses the native retained-state\npolicy instead. None of these labels confirms seat exit, authority retirement or cleanup. From\nmanager cluster revision 33, a manager that is not `active` and holds a preservation inventory also\nreports `preservation`: the attempt and the inventory digest a preserved-exit stop names. A\npreserved-exit stop request carries that binding with `withAgents: false`; the manager accepts it\nonly when it is fully preserved, whole-manager and quiescent, and its result is\n`preserved-exit-returned`. The open-space local receipt keeps its revision-32 shape.\n\nThe manager resolves visibility through the same fresh owner/admin policy as `ps`. An\n`owner-filtered` observation is not the full manager impact, even if its rows happen to match a\ncount. A refused stop reports the selected manager, visibility, maintenance state and pending-work\ncounts that blocked its preflight, without exposing other owners' agent rows. Owner-filtered\nvisibility requires the stack operator or an authorized admin-scoped caller; the command never\nsubstitutes one. No stop policy is accepted while any blocker remains. This is\nan observation at the reported time, not a reservation, permission to stop or a guarantee that the\nset cannot change. Older managers can omit it; the native service adapter refuses when the\nrequired process/lifecycle correlation is absent. Generic process launch/stop still refuses managers.\n\nThe `manager` service preset uses the `manager-service` adapter. It requires the selected registered\n`root` and an explicit installed `runtime`; an optional `server` can repeat the registered broker.\nThe optional `console-port`, `console-host`, `ws-port` and `max-sessions` pass explicit native\nsettings. Omitting the host keeps the native loopback binding; port zero requests an allocated\nconsole port. A declared `launch` also requires `launch-sha256`. It names an existing private\nresolved launch spec, pins its full SHA-256 and must name this space. The child parses those\nverified bytes before authority preparation and keeps that snapshot through startup. An optional\n`roster` names an existing [roster file](define-a-team.md#roster-files) by absolute path. Only its\nowner or root may write it or its directory. The child parses it before startup, and each launch\nboots the agents it lists the way `supervise --roster` does. A resumed manager takes no roster.\nSaving these options does not prepare a manifest, provision an agent or prove that requested agents\njoined. Saving it starts nothing and grants no authority. Its native child starts no broker or\ndelivery daemon, does not adopt an existing legacy manager, and never prints its private console URL.\nIt declares one instance per definition and runs in the background or with `start --foreground`.\nIt is not OS-supervised installation or platform parity.\n\nThe real child exclusively claims its creation-pinned native PID custody before hosted authority\npreparation or local endpoint initialization. That starting record is not readiness; another existing slot is not adopted or reclaimed.\nAfter a crash, the record the killed child left names a dead pid. The manager adapter removes that\nrecord before the next launch, so the next `up` relaunches the manager; a live, foreign or\nunreadable record still refuses.\nIt binds its actual native identity before endpoint construction. A startup phase appears only\nafter native start, settled custody and signal handlers. On authenticated meshes the adapter also\nrequires a current status response pinned to that native instance and epoch, with the same process\nlifecycle UID. It preserves the invoking managed-seat caller, registered root/broker and required\nTLS. Missing caller support refuses without an operator substitution. The completed status read's\nown admission is excluded from stop impact; other admitted work remains counted.\n\nAn open mesh uses the pinned child's private request-bound local observation. Its service, launch,\nrequest and both process creation identities must match, and its registration must match the\nlaunch identity. Output labels this a child-local receipt, not a broker-attributed endpoint\nresponse. A retained stop handoff blocks readiness adoption on either route instead of treating\na live PID as a new serving launch.\n\nA real `service stop` or `restart` reads complete caller-visible impact before confirmation.\n`--with-agents` requests native agent stops, not generic descendant signalling. An interactive\nconfirmation defaults to cancel and shows every affected identity, lifecycle and requested seat\naction. Scripts require `--yes`. The offline `--dry-run` still imports no provider and performs no\nnative impact query; it is not stop authorization.\n\nThe native stop reserves its original PID custody without reclaiming an existing reservation.\nIt rechecks the confirmed scope, binds the full service/launch/stop request and both process creation\nidentities, and uses the existing native shutdown intent for agent reap. The child rechecks the\nsame complete scope before accepting. Changed or incomplete scope refuses without a broader policy.\nThe controller requires immutable accepted/returned evidence and the original process exit before\nrecording a native stopped state. An unconfirmed request retains its reservation and never escalates\nto force kill, restarts, or treats PID disappearance as completion. A later bare `up` or `down`\nsettles it only from the same evidence, and only then releases the reservation. Reported cleanup remains not\nchecked. Retained native-policy markers prevent a metadata change from selecting generic signals,\nand an unconfirmed native stop cannot be forgotten just because its process disappeared. Legacy shutdown refuses the native child rather than bypassing that boundary.\n\n### Resolving endpoint targets\n\n`endpoint describe <target>` and `endpoint invoke <target> <command> [--args '<json>']` use the\nexisting native caller and digest-verified contracts. `ref:<name>` requires a saved reference.\n`ep:<class>` bypasses local references. An unprefixed name checks the selected space's saved\nreferences first, then uses the native class only when no reference exists. An unreadable or\ninvalid reference refuses instead of selecting another target.\n\nA selected reference is disclosed on stderr and in `result.resolution`, with its revision and\nresolved native class, instance and epoch. The CLI holds its cooperative writer lock and checks\nthe same bytes before invocation. `--on` must agree with a saved instance pin. A missing pinned\ninstance does not fall back to the class queue. Unknown native effects are not blindly retried.\n\nA reference's file credential uses the [paired native caller adapter](#endpoint-caller-credentials).\nIt must match the selected registered static root, space, endpoint and signed native scope. A\nconflicting explicit `--creds` refuses. Arbitrary raw files do not supply a caller triple, and a\nreference's instance pin cannot be dropped to fit a class-only file. Neither case substitutes an\noperator identity. References without a file use the normal registered-space caller and its\nexisting capability checks. No local reference grants remote authority.\n\n### Declared capabilities that remain gated\n\nThe CLI grammar also names `endpoint watch <target> <record-key-or-topic> [--since <native-sequence>]`,\n`endpoint session <target> <command>`, `endpoint work lease <target> <pool>` and\n`endpoint work commit <target> <pool> --lease <private-file> --result <JSON>`. They return a typed\n`UNSUPPORTED` result before any broker connection, subscription, cursor, session, lease or commit.\nThe current resolver supplies command contracts, not a verified mapping of these application\ncapabilities. This is a CLI integration limit, not a claim that the selected endpoint lacks them.\nNo arbitrary subject, pool implementation or remote execution is guessed.\n\n`endpoint invoke --handle <private-file>` is also an explicit unsupported boundary, because no\nverified native auth-slot adapter is connected. It opens no file, redeems no handle and publishes no call.\nAn opaque low-level auth string alone does not establish receiver-side attenuation. Offline\n`auth handle inspect` remains explicitly unverified metadata without `--space`; its online\nstatic-registry signature inspection also does not establish presenter binding or use authority,\nand is not a substitute for that gate.\n\n`endpoint describe --refresh` refuses as unsupported and sends nothing. This CLI keeps no resolved\ncontract between calls: each describe makes a fresh describe request and fetches the contracts\nagain, so there is no cache for the flag to bypass.\n\nA saved-reference or paired-file invocation uses the described instance and epoch without bind-repair\nto a different handle. A pre-effect bind refusal remains a refusal, not permission to choose another\ninstance. Invocation receipts attribute resolution to the native responder that answered.\n\n`endpoint --live` adds authorized native service-registration metadata to the local reference\ninventory. It uses the existing scoped service-record read rows, without adding grants or reading\nmessage bodies. The SDK paginates the filtered metadata list; per-key spec/status reads come from\nthe leader. The list can lag concurrent changes and is not an atomic whole-space snapshot.\n\nThe snapshot reads at most 1000 registration keys. Omitted keys, unreadable records or unavailable\nregistry access make the requested live portion partial and return exit 4, while preserving the\nlocal references. Status projection, reported state, process ownership and a serving response are\nseparate. No active probe or process control is implied by a registration row.\n\n### Clearing retained history\n\n`history clear` purges all retained channel history in the selected space. It takes no channel\nargument. `--dms` also requests DM history under the existing purger authority. In a terminal,\nconfirmation names the space, broker and space-wide scope and defaults to cancellation. Scripts\nand `--json` require `--yes`. The old `--force` spelling refuses with the replacement.\n\n`history clear --dry-run` resolves the target and names the affected streams offline. It issues no\ncaller credential, connects nowhere and does not check authorization or retained message counts.\nA space that is prepared but not started is refused with `TARGET_UNAVAILABLE` (exit 2), and the\nrefusal says how to start it.\nThe purge uses the existing purger instrument or one-action user purger view, not a broader caller.\nOn a user-auth mesh whose grant lacks scope `admin`, the purger view is not issued and the refusal\nis `PERMISSION_DENIED` (exit 2). Its remedy is the identity provider's own sentence, which names\nthe missing scope and the whole-row re-grant that adds it.\n\nJSON and plain receipts retain each native stream acknowledgement and message count. A later\nrefusal after an acknowledged purge is partial. An unconfirmed submitted purge is unknown and is\nnot retried. Clearing history does not stop agents, remove stream definitions or reset identity;\nnew messages may arrive after the acknowledged purge.\n\n### Following an endpoint action\n\n`endpoint invoke <target> <command> --wait` requires the command's digest-verified action marker\nand the native goal-result read surface. The CLI submits once, retains the accepted goal ID and\nfingerprint and follows through the native helper. Reconciliation reads the accepting instance,\nnot an unrelated class winner. A reference keeps its described binding for submission.\n\n`--wait --json` emits goal NDJSON. It exports the original request, acceptance and observed terminal\nresult. Intermediate native progress is not exported; a gap record states that limitation. No\nglobal sequence or task progress estimate is invented. An unknown observation keeps its accepted\nidentifiers and is not retried. Ctrl-C or termination ends observation, not the underlying action.\n\nGoal-stream terminal values are capped at 16384 UTF-8 bytes. A larger value is omitted with an\nexplicit gap and retained goal identity, not split into invalid JSON. Read that original goal's\ncanonical result if needed; do not resubmit an action to recover its output.\n\n### Unconfirmed requests\n\nClassified failures use the same report layout as successful commands. The report names the\noutcome and stable code, then shows available request, goal and native responder attribution.\nUnknown and partial results use waiting treatment, not a healthy completion marker. Plain mode\nkeeps the same state words without ANSI. The JSON envelope stays on stdout; diagnostics stay on\nstderr. Raw argument objects and arbitrary native detail payloads are not copied into the human\nreport. Confirmed history purge acknowledgements remain visible when a later stream fails.\n\n### Persona previews\n\n`spawn --file/-f <persona-file>` selects a persona, not a team manifest. It cannot be combined\nwith a positional persona or the persisted internal `--config` spelling. A bare filename is\nstill a file, including when it has no extension. Managed paths remain relative to the manager's\nworkspace and are read on that execution host. No file upload is implied. Direct foreground\nexecution reads its local file. A supported Mesh-manifest shape returns a schema refusal with\nthe literal `cotal team deploy <file>` remedy, before agent allocation.\n\n`spawn --dry-run` never refreshes the catalog, repairs stale registrations, imports connector code,\nconnects to the broker, redeems enrollment, provisions or allocates an agent. The managed preview\nis currently partial: it retains requested selectors but cannot resolve host-local persona and\ncwd facts offline. It does not substitute the caller's files. With `--foreground`, the preview\nresolves the local persona, harness precedence and physical working directory. Binary, login,\nmodel-policy and live authority checks are labeled not performed.\n\n`spawn --foreground --enrollment-file <private-file> --space <name>` uses the existing one-seat\nforeground enrollment path. The file is opened with protected ownership/mode and bounded input\nchecks; its URL is not printed, copied into the child environment or persisted by this input\nadapter. It does not supply a managed enrollment authority path. Dry-run does not read or redeem\nthe enrollment file. On Windows the file must be a regular file; its owner, mode and links are not\nchecked.\n\n### Owned process services\n\nPreset inventory checks the selected installed declaration without importing provider code. It\nshows declared foreground/multi-instance capabilities, process versus endpoint readiness, and\nnative-request-bound versus direct-process stop policy. Missing, skewed or unreadable metadata\nleaves the inventory partial, with unknown capability fields. Passing metadata checks is not\nproof that an application, dependency or service is ready. Linux direct-process\ninstances can use the owned systemd user-unit route described below. An adapter\ndoes not acquire that mechanism from its name or preset.\n\nThe direct-process driver pins Linux machine/boot IDs and process start ticks.\nmacOS and Windows still refuse: their generic process custody needs a stable\nmachine, boot, owner and subsecond creation pin before any signal is safe.\nThe existing macOS `ps -o lstart` timestamp has only second precision, so\nhashing it would not distinguish a PID reused within that second.\n\nInstance reports separate recorded supervisor policy from supervisor discovery, which remains not\nchecked by this direct-process driver. A recorded `none` means no supervisor assigned by this\ncontroller. It is not proof that an external unit cannot supervise or restart the process.\nA retained supervisor receipt binds its definition revision, full instance, machine and OS user.\nIts file view hashes only the recorded physical unit, without printing its body or environment.\nMatching bytes are not loaded/active state or enabled-autostart proof. Missing, modified or\nunreadable unit files do not turn a receipt into an uninstalled service. Direct start/stop/remove\nrefuse while such a receipt is retained; they do not launch beside a unit or implicitly uninstall\nit. `service status` can read an unchanged recorded systemd user unit on the same host through\none bounded exact-unit query. It checks the returned physical fragment and reports native state,\nmain-PID attribution, invocation, reload need and presence of unverified drop-ins. Missing or\nambiguous observations stay partial. It neither prints unit/environment contents nor discovers\nownership from names or cwd. This observation proves neither endpoint readiness nor effective\nlogin/reboot autostart. Inventory and direct-process dry-runs perform no OS query;\nLinux native-unit removal dry-runs make one bounded exact-unit status read.\nOn Linux, `service install <name> --instance <full-id> --dry-run` asks the\nsystemd user manager for its config directory, then renders a\nbounded direct-process unit and private environment candidates and reports\ntheir paths and digests without printing their bytes or the inherited `PATH`.\nWith no existing instance, the inspection checks the declared process,\ndurable CLI entry and user manager, then reports that apply would allocate a\nstopped full ID; it\ninvents neither an ID nor a unit candidate. On macOS the same preview renders\na bounded LaunchAgent plist candidate for a settled stopped direct-process\ninstance, with its label, path and digest but not its body or inherited `PATH`.\nIt checks the `Library/LaunchAgents` segments without following symlinks;\nit does not query a launchd domain, load a job or enable autostart. An existing\nplist remains unattributed. With no instance it reports a possible stopped-ID\nallocation, not a plist. macOS install apply refuses, because it has no native\ncontroller or custody records.\nLinux bare install allocates that stopped full ID only after its source validation;\nit does not start a process. If later installation fails, the allocation is\nreported as a changed resource rather than hidden or retried.\nThe unit environment pins an absolute-entry `PATH` from this invocation;\nrelative or empty entries refuse rather than acquiring another working\ndirectory at boot. It requires a settled stopped instance, its current\ndefinition revision and no recorded supervisor, host environment reference or\ncredential reference. An existing unit at that path remains unattributed, not\nowned. The directory's parent ownership, private seed, receipt intent, loaded\nunit and autostart are not established; the preview is partial and authorizes\nno install. Linux apply retains a per-step intent before exclusive private file\nwrites, isolated connector seeding, a bounded private-tree fingerprint, user\nmanager reload, enable without `--now`, and the exact supervisor receipt. It\nreturns the full instance and observed unit-file state, not a process-readiness\nor effective reboot-autostart claim.\nA retained installation or removal intent blocks a new install and is reported\nas partial by `service status` with its exact unit path, private-store path,\ncurrent step and last acknowledged step. A bounded native read for each retained install, removal or start/stop/reseed\naction distinguishes an unchanged physical unit fragment from a name-only\nstatus when the file is missing or modified. It also classifies the private\ndirectory without inspecting its children. None of these reads permits an\neffect to resume. It cannot\nauthorize a blind retry. A changed\npre-effect target leaves a planned step and a partial receipt; an unconfirmed\nsubmitted effect is unknown and is never automatically repeated.\n`service remove` refuses while any native attempt names the definition. There is no\nexplicit reconciliation for an uncertain attempt.\n`service uninstall <name>\n--instance <full-id> --dry-run` reads only a matching recorded systemd unit,\nits private-store directory and one native status snapshot. A retained native\naction, or missing, modified or foreign receipt files, refuses. An owned private\ndirectory does not prove its descendants unchanged. A separate unexposed\nremoval candidate requires a bounded no-follow fingerprint of the complete\nprivate tree to match the install receipt. Changed content is preserved and\nremoval refuses. The preview does not reserve a stopped unit, enable state or\nremoval authority; its report remains partial. Linux apply requires the exact\nunchanged loaded inactive unit and private tree, with `--yes` in scripts or a\nscoped terminal confirmation. It disables autostart, removes only the pinned\nunit and private tree, reloads the user manager, then removes the supervisor\nreceipt. The definition and stopped instance record remain.\n\nA retained native start, stop or reseed action is reported as partial by\n`service status`; it does not authorize another request. Native start and stop\nrequire the exact owned receipt and current native unit observation. An older\nprivate store is reseeded while the unit is stopped, with a separate retained\naction and refreshed generation/tree digest before start. An unconfirmed seed\nis never retried. Restart uses the same exact stop and subsequent start; if\nstart does not confirm, the stopped original remains in its result.\nUninstall retains a step before disable, exact unit unlink, reload, private\ntree removal and receipt deletion. A non-confirming step remains visible in\n`service status`; a missing unit alone never clears its attempt.\n\nThe private unit entry resolves only its built-in command and validates the explicit controller\nhome, space, definition and full instance before any extension overlay, repair or package load.\nIt refuses an inherited managed-spawn handoff without reading or deleting that file.\nBefore native replacement, it requires the recorded private seed generation,\nexecutable provenance and absence of unfinished seed markers; it never reseeds\nthat store at boot. For a\nrecorded direct-process unit it also requires no retained installation or\nremoval intent, the current native unit invocation and matching physical\nfragment before replacing itself with the saved executable. It does not fork a detached\nwrapper or retry execution. A starting record does not establish successful replacement or\nreadiness; an interrupted replacement remains unknown. A later unit invocation refuses an\nunsettled starting, stopping or unknown attempt instead of replacing its request.\nInstall dry-run makes only the bounded native directory read above; uninstall\ndry-run reads the exact recorded unit and private-store directory. Neither\nallocates an instance, enables autostart nor removes a receipt or unit.\n\nProcess definitions support `service start/stop/restart/status/logs <name>`. The driver currently\nrequires Linux stable machine identity, boot identity and readable process-creation tokens. It\nrefuses another platform instead of using PID-only ownership. It launches executable and argv\nseparately without a shell. No broker, manager or other dependency is started implicitly.\n\nInstance IDs are assigned once and printed in full. With several records, scripts need\n`--instance <full-id>`; a terminal can choose from those IDs. `start --new-instance` explicitly\nrequests another process-driver instance. Otherwise one serving instance is a no-op and an\nunresolved prior launch blocks another allocation. A launch that failed before its child existed\nis recorded as failed, not unresolved. A stopped instance retains its logical ID\nand receives a new launch request. Unknown startup retains its receipt and is not resubmitted.\n\nOnly safe base environment names and the definition's selected `envRefs` are passed. A declared\nprotected credential file is forwarded to that program as the `COTAL_CREDS` path, not credential\nbytes. Control credentials are not inherited wholesale. The program still runs as the current\nOS user; this is not a process sandbox. Native managers require their service adapter and\nagent-impact guards and are refused by generic process launch/stop.\n\nAdapter definitions use the same owned direct-child controller. The local-process provider\nadds a service ABI declaration and launch/readiness hooks. Its closed option schema supports\nbounded objects, arrays and scalar fields, with no schema references, regular expressions,\ncoercion, defaults or unknown-option removal. Every object closes its declared fields. An\nunsupported schema refuses rather than being widened. A declared path option and its ancestors\nfollow the [`space create`](#selecting-a-space) rule for other local writers; a private file stays\nowner-only.\n\nThe provider declares foreground, multi-instance and log capabilities. An explicit start records\nits definition revision, full instance, launch request and adapter package/version/declaration\nfingerprint before loading provider code. The launch hook receives the definition's `envRefs`\nnames, never their values, and returns executable, argv and working directory separately. It must not launch children, mint credentials or start dependencies while\nplanning. A changed adapter is not adopted by a retained instance. Native stop declarations add\nclosed impact and terminal hooks; a missing hook refuses instead of using generic signals. The\nmanager's adapter supplies its own native policy and receipts. A preset is not installed under an\nOS supervisor.\n\nReadiness hooks receive the pinned process and a bounded cancellation signal. Ready observations\nmust match that process and carry a current observation time. An endpoint-serving declaration\nalso returns its checked native class, instance, principal, lifecycle and epoch. The adapter must\ncorrelate those native coordinates with its owned launch using its supported authority; a matching\nname or successful HTTP request is insufficient. Reports preserve those coordinates and their\nobservation time. Local status does not refresh them or promote them to current live authorization.\nA failed or timed-out hook retains an unknown receipt and is not retried. A cancellation signal\ndoes not prove third-party code stopped working.\n\nExtension removal checks retained service records in every local service space, including spaces\nabsent from the mesh-selection registry. Live, foreign, unreadable or unresolved instances block\nremoval. A starting intent and the extension writer lock prevent an uninstall from crossing the\ncontroller's final provider check. Providers still run as the current OS user, not in a sandbox.\n\nProcess readiness reports a live creation-pinned direct process as Running. HTTP readiness uses\na bounded loopback request with credentials omitted and redirects disabled. Its success is\nReachable, not proof that the child owns that listener or serves a native endpoint. Status labels\nthe stored readiness observation and its time separately from the current process observation.\n\nStop and restart default to cancellation in a terminal and need `--yes` in scripts. The selected\nrecord revision and creation identity are checked before signals, including escalation. A reused\nPID is never signalled as the old process. Restart holds the definition operation across the\nconfirmed old-process exit and one replacement launch. An unconfirmed stop cannot cause a\nreplacement. `--dry-run` creates no instance and sends no process signal.\n\n`start --foreground` is an owned foreground process, not an attachable view. It rejects JSON and\ncannot adopt an already running background instance into the terminal. Before allocation it prints\nthe terminal lifetime and output policy. Native auth and Linear presets use their installed signal\nhandlers through this same flow; neither endpoint readiness nor native cleanup is inferred from\nprocess exit. A foreground manager stops through the same request-bound native stop as\n`service stop`, keeping its agents: the SIGINT or SIGTERM the CLI receives is the confirmation.\nCtrl-C also reaches the native child, which ignores a signal that carries no stop request and\nprints one warning line for it.\nNative service child boot never repairs or seeds connector packages. Background stdout/stderr\ngo to owner-private logs. `logs --tail <1..1000>` reads a bounded tail; omitted earlier bytes make\nthe result partial. Foreground output is not captured there. Log text is not promised to be\nsecret-redacted. Live following uses the bounded read-only stream described below.\n\nFor a recorded systemd user unit, logs use its saved original invocation, boot and OS user,\nnot the native manager's current invocation. Finite tails read at most 1 MiB and 1000 records.\nIndividual native JSON records are capped at 256 KiB. Native journal cursors and microsecond\nwall/monotonic timestamps are kept; binary or repeated message values stay native JSON. Missing\ninvocation evidence refuses without substituting an old direct-child file. Native diagnostics are\nwithheld, not suppressed by the query; their presence makes a finite result partial.\n\nJournal following freezes those same selectors. It reports oversized, incomplete or unattributed\nrecords as gaps and emits native cursors, rather than inventing a sequence. Ctrl-C closes only the\nowned journal reader. It never signals the service or adopts a later invocation. This scope includes\nreadable process-origin messages, not every user-manager lifecycle diagnostic. Journal access and\nretention may omit messages, and an empty result does not prove there was no output or work.\nJournald must retain an accessible per-user journal; no alternate file, privilege escalation or\nremote log service is used. These are authorized local logs and can contain secrets.\n\nThe process driver tracks its direct child, not arbitrary descendants a program creates. A\nstopped/direct-process-gone observation must not be interpreted as proof that all external work\nor descendant processes ended. The process driver itself provides no service presets, agent-aware\nmanager control, owned OS units, non-Linux lifecycle parity or full preset and OS-supervisor\nintegration.\n\n`service logs <name> --instance <id> --follow` follows that private file without controlling the\nservice. JSON emits log snapshot/event/connection/gap/error/end records. Events carry native file\nidentity and byte offsets, not a manufactured global sequence. Initial tails and reader backlog\nare bounded at 1 MiB and chunks at 16384 bytes, with explicit gaps for omissions, replacement or\ntruncation. Instance-generation changes are attributed. UTF-8 decoding can replace invalid bytes\nand is labeled accordingly. Output backpressure pauses the reader instead of growing a buffer\nwithout bounds. Ctrl-C, termination and closed output end the view, not the service.\n\n`auth retire <full-lifecycle-uid> [--space <space>] [--yes]` validates the native UID and resolves\nthe selected auth target. The CLI then refuses unsupported caller context: the registered\nauth endpoint requires the requester's own current serve identity/gate, which the standalone CLI\ndoes not provide. It neither borrows a manager identity nor substitutes the interactive/managed\nloopback host doors. `--yes` cannot create authority, and no retirement request is sent.\n\n`auth redeem <private-handle-file> [--space <space>]` selects the existing local auth target and\nrefuses with `UNSUPPORTED` because its conferring-handle redemption path is not exposed. It does\nnot read the file, consume a handle or issue credentials. Session grants, agent enrollment and\nparsing an unverified handle are not substituted for that authority.\n\nNested help follows declared command words, for example `cotal help auth handle inspect`.\nAliases resolve within their declared parent. Help reads cached metadata and imports no provider,\nstarts no service and opens no credential file. A command's `-h` selects the deepest declared\nmode before its data arguments; it does not execute that mode. Scoped options refer to the\ncommand's shared flag definitions. Completion uses the same declared subgroups and option names,\nwithout treating an option's value as another command word.\n\n\n### Endpoint class scatters\n\n`endpoint invoke <target> <command> --all` submits one native ephemeral class scatter.\nIt freezes the expected live instance set, gathers attributed replies and reconciles registration\ncurrency through the native SDK. It does not probe additional instance rails or retry a mutation.\nInstance pins, targeted commands, `--cast` and `--wait` cannot combine with a class scatter.\nThe installed helper does not submit journaled commands through this route.\n\nFinite JSON retains the original request, each valid member result and the native missing, churn,\nunexpected, duplicate, late and invalid buckets. Invalid-reply diagnostic bodies are omitted.\nCoverage and command success are separate: complete coverage can still contain refusals or failures.\nAll confirmed pre-execution refusals exit 2 (invalid input exits 1), all executed failures exit 5,\nunconfirmed execution exits 3 and mixed or partial outcomes exit 4. Reports keep full IDs, show\nbounded member/observation rows and state their omissions. A missing reply is not proof of death.\nResult values are endpoint-provided and are not guaranteed to be secret-redacted.\n\n\n### Personas on an exact manager\n\n`persona --on <full-manager-instance>` lists that manager's authorized catalog.\n`persona show <name> --on <id>` reads a card only when its native ownership policy allows it.\nA name-only row does not grant access to its instructions or permission to redefine it.\nThese operations never substitute the local catalog or another manager.\n\n`persona create <name> --on <id>` accepts one of `--file`, `--prompt` or `--from`, with\n`--from -` as explicit piped input. Leading frontmatter follows the native persona merge:\nthe supplied name wins, explicit model/role/application and channel fields override the file,\nand the manager preserves existing ownership and redefinition policy. Inputs are bounded to\n1 MiB and must be valid UTF-8. A malformed leading block refuses before the remote request.\n\nThe remote wire accepts name, body, model, role, application, initial read channels and read/post\nACLs. `--harness`, `--subscribe`, `--allow-subscribe` and `--allow-publish` name those fields.\nOther frontmatter, including kind, tags, description, attention settings, variant, launch options,\ncapabilities and owner, refuses as unsupported instead of being silently discarded. Remote edit\nand removal are not native wire operations and refuse; no local file is changed instead.\n\nCreation is silent unless `--announce <concrete-channel>` is supplied. Announcement preparation\nmust resolve to the same space, root and account context before the write. A confirmed save and\na later denied or unconfirmed announcement produce a partial receipt retaining the save. Do not\nrepeat the definition to recover an announcement; read the channel first. A stored announcement\nis not delivery, a spawned agent or task completion. Finite JSON/plain receipts keep the requested\nand answering manager, epoch and original request, without printing the creation input body. Rich results offer\nfollow-up show or spawn commands with the original exact manager pin; a suggested launch still\nrequires the caller's native spawn authority.\n"
|
|
65276
|
+
"body": "# `cotal` CLI reference\n\n> **Reference**: describes the TypeScript reference implementation (the `cotal` CLI), not the wire contract. \xB7 **For:** operators \xB7 **Wire contract:** [SPEC](../SPEC.md)\n\n`cotal` is the operator command line for the reference implementation: bring a mesh up, mint\nidentities, launch agents, watch what they do, and tear it all down. It is a thin client over the\nwire contract: the normative subjects and schemas live in the [SPEC](../SPEC.md); this page is\nlookup material for the commands, not a walkthrough; if you are new, start with\n[Getting started](getting-started.md).\n\n## Running it\n\n```bash\nnpm install -g cotal-ai # puts `cotal` on your PATH (needs Node 22+)\ncotal # the selected space, common commands, and where to start\ncotal help # compact actions and selected context (also `cotal --help`)\ncotal help --all # every command group, including hidden commands\ncotal help <command> [<subcommand>] # one command's or subcommand's usage and flags\ncotal version # cotal-ai version + each installed extension's (also `cotal --version`, `cotal -V`)\ncotal version --json # the same as one JSON document (also `-j`)\n```\n\nThe shell installer prints its absolute launcher path. Use it when an older installation still\nwins plain `cotal`. Having `~/.local/bin` somewhere on PATH does not establish precedence, and a\nchild installer cannot change the parent shell's command cache, aliases or functions. Inspect\n`type -a cotal` in that shell. After putting the new directory first, refresh its command cache\nwith `hash -r` (or `rehash` in zsh). An alias or function must be corrected separately. To restore\nconnectors after an old executable rebuilt them, run the verified newer absolute executable with\n`ext seed`, without `--force`.\n\n`npx cotal-ai <command>` runs it without a global install. Bare `cotal` starts nothing: it names\nthe selected space, lists common commands, and points a machine with no selected space to\n`cotal setup`. Every command generates its own help, usage, and shell completion from its declared\nflags, and `cotal <command> --help` prints the same help as `cotal help <command>`. `-v` is not a\nversion flag.\n\nSome commands have short aliases, listed in `cotal help --all`: `sp` (spawn), `a` (attach),\n`s` (status), `r` (run), `x` (ext), `h` (history), `cfg` (config) and `p` (persona). Subcommands can have their own, such as\n`cotal ext rm` for `cotal ext remove`. An alias behaves the same as its command. A command or\nsubcommand is never selected by a prefix, so `cotal spaw` is an unknown command. A command, alias,\nor extension command that would claim a word another one already claims is refused when it\nregisters or installs.\n\nIn a dev clone, `pnpm cotal <command>` runs `bin/cotal.ts` through `tsx`, but `bin/` loads the other\npackages from their built `dist/`. Run `pnpm build` after `pnpm install`, and again after you edit a\npackage, or the command fails with `ERR_MODULE_NOT_FOUND` for `@cotal-ai/cli/dist/index.js` or runs\nthe old code. A Windows source build needs the matching MSVC Developer Command Prompt to compile\nits first-party process identity binding. Published packages already contain the x64 and arm64\nbinaries and need no compiler or native download.\n\nCommands that change the installation run the automatic connector seed and stop when the\noperator-global seed store is not current for the checkout's version, which includes a fresh config\nand a store another version wrote. The error includes\n`this would migrate the machine-wide store and can take an installed cotal down`. A\ncheckout never writes the connector store an installed `cotal` uses. For day-to-day\nwork, set `COTAL_SKIP_CONNECTOR_SEED=1` to skip that seed step:\n\n```bash\npnpm install\npnpm build\nCOTAL_SKIP_CONNECTOR_SEED=1 pnpm cotal status\n```\n\nPointing `XDG_CONFIG_HOME` at a scratch dir is not enough by itself. Isolated release tests\nthat must seed from a checkout also set `COTAL_ALLOW_CHECKOUT_SEED=1`, as described under\n[Built-in connectors are seeded extensions](#built-in-connectors-are-seeded-extensions).\n\nBefore publication, the release workflow installs the packaged candidate into fresh homes and\ncaches on Linux, macOS and Windows, on x64 and arm64 with Node 22, 24 and 26. Each setup seeds\nconnectors, starts the broker, delivery daemon and Manager, checks native process identity and\nManager control, then stops and restarts them. Every platform must pass before normal or snapshot\npublication can proceed.\n\nAn undeclared flag is a usage error, and so is a flag given more than once unless it is\nrepeatable, as `--opt` and `down --session-store` are. So is an unknown command, and a first word\nthat is not one of the command's subcommands when it declares them. So is input a command checks\nbefore it acts, such as a missing argument, an invalid flag value or two flags that conflict, and\n`cotal help` with a subcommand that does not exist. The command prints the error, then any remedy,\nthen its help on stderr, writes nothing on stdout, exits 1, and does not run. A usage error is found before any effect, so it never seeds\nconnectors or writes configuration. Help, version and completion never do either.\n\nCommand output, including error lines on stderr, the guided `setup` and `meshes add` prompts and\nthe `console` screen, is colored only when stdout is a terminal, so piped or redirected output is\nplain text. A non-empty `NO_COLOR` turns color off on a terminal too. `FORCE_COLOR` turns color on even when output is\npiped, unless it is `0` or `false`, and it takes precedence over `NO_COLOR`. `--no-color` turns\ncolor off for that run, whatever the environment says. It sets no `NO_COLOR` for the programs the\nrun starts, but they are no longer forced into color by `FORCE_COLOR`.\n\nAt any question the CLI asks, Ctrl-D on an empty line or an input that closes cancels the question\nas Ctrl-C does, so the command reports its own cancelled outcome.\n`--no-input` means the run never prompts, opens an editor or opens a browser. A command that would\nprompt behaves as it does without a terminal, `web` prints its link without opening it, and\n`personas edit` is refused. `--json` implies `--no-input`. `--no-color` and `--no-input` work before\nthe command word on every command, and after it on every command except one that takes its words\nraw. After `--` they are ordinary words. No command may declare a flag with either name. The output\nflags `--json` (`-j`) and `--plain` may also come before the command word, as in\n`cotal --json status`; they are read as the command's own flags, so a command without that output\nrefuses them as it does after the command word. A command that takes its words raw, such as the\nhidden completion entry, has no flags to read them as and refuses them before its word with a usage\nerror. A `--` before the command word ends these leading flags. An installed extension\nwhose command declares one is an internal error, and `cotal` exits 1 before running any command.\nShell completion treats the current `--` word as a flag prefix until it is committed. After `-- `,\nit completes positional values without offering options. A string option's value is completed\nfrom its own candidates or file hint, without inserting other option names.\n\nCommands come from the surfaces the binary composes: the base mesh CLI, the manager and the\ndelivery daemon, plus any operator-installed extensions. Hidden entries such as the deprecated\n`supervise` and `deliver` are not completed.\n`cotal ext add <npm-package>` installs any registry providers a package contributes: commands,\nruntimes, and local process lifecycle descriptors. The `web` dashboard and optional manager\nruntimes ship this way. A `@cotal-ai/*` package named without a version installs this cotal's\nversion, as `setup` and `update` do; a version or tag you name is installed as given.\n\n## Structured output\n\nA command whose help has a `structured output` line naming `json` takes `--json` (`-j`) and writes\none JSON document on stdout. Most commands have one, among them `cotal status`, `cotal space`,\n`cotal service`, `cotal up`, `cotal down`, `cotal ps`, `cotal history` and `cotal config`.\n`cotal help <command>` shows whether a command and each of its subcommands has one.\n`cotal version --json` writes:\n\n```json\n{\"schemaVersion\":1,\"command\":\"version\",\"target\":null,\"outcome\":\"ok\",\"completeness\":\"complete\",\"result\":{\"version\":\"<version>\",\"extensions\":[{\"package\":\"@cotal-ai/web\",\"version\":\"<version>\"}]},\"warnings\":[]}\n```\n\n| Field | Meaning |\n|---|---|\n| `schemaVersion` | `1` |\n| `command` | The command that ran |\n| `target` | What it acted on, or `null` for a command that reads only local metadata |\n| `outcome` | `ok`, `no-op`, `refused`, `failed`, `partial` or `unknown` |\n| `completeness` | How much of the read scope the result covers: `complete`, `partial` or `not-checked`. It says nothing about whether the work succeeded |\n| `result` | The command's result. A `partial` read keeps what it read; a refused, failed or unknown run has `null` |\n| `warnings` | One `{code, target, message}` for each thing it could not read or check |\n| `error` | Present when the outcome is not `ok` or `no-op`: `{code, message, remedy, cause}`, where `cause` keeps an endpoint's own code, outcome and safe detail |\n| `changed` | What a command that did not succeed changed anyway |\n\nDiagnostics stay on stderr. A usage error under `--json` is a `refused` envelope with code\n`USAGE_INVALID`, and the help it names stays off stdout. A command without a JSON output refuses\n`--json` as an unknown option.\n\nA subcommand's help has its own `structured output` line, and only the forms that show one write a\ndocument. Bare `cotal ext` and `cotal ext list` do. `ext add`, `ext remove`, `ext root` and\n`ext seed` report as text, refuse `--json` and `--plain` with a usage error, and exit 1 on any\nfailure.\n\nA command whose `structured output` line also names `plain` takes `--plain`, which writes the same\nreport as plain lines: no color, no marks, ASCII labels. Plain writes these punctuation marks in\nASCII wherever they appear: `\xB7` and long dashes as `-`, `\u2026` as `...`, `\u2192` and `\u21B3` as `->`, and `\xA7`\nas `section`. Other characters, such as the letters of a name or a message, are written as they are.\nPass one of `--json` and `--plain`; both is a `USAGE_INVALID` refusal. A table that does not fit the\nterminal is written as one labeled record per row, so no value is cut off.\n\nShared human reports for a resolved space begin with `Mesh: <space>`, including\nempty `ps` results, manager inventories, the impact a command shows before it\nstops something, and classified failures. A failure raised after the command\nresolved its space carries that space in `target.space`, even when the failing\nstep named no space. The label comes from the operation's target, not a new read\nof the current selection. Global reports do not invent a selected mesh. JSON\nretains `target.space` without a text header. Plain watch feeds and followed\nhistory prefix each new row with the mesh name so the context remains visible as\noutput scrolls.\n\nOn a terminal, a long value, line, warning or failure wraps at spaces, and each continuation line\nstarts under the text it continues. A word wider than the line, such as a path, keeps a line of its\nown and is never split. Help wraps the same way, including the title of `cotal help --all`.\n\nA usage line breaks only between options, never between an option and its argument. A ` | ` stays\non the line before it whenever it fits there. A compact `|`, such as the one in\n`<persona.md>|--prompt`, is a break point too: the line ends right after it, the next line starts\nwith the following option, and no space is added or lost. A usage too wide for the line breaks after\nits own ` | ` or compact `|` alternatives first. A group too wide for the line, such as\n`(--to/-t <agent> | --channel/-c <channel> | --role/-r <role>)`, takes lines of its own and breaks\nthe same way inside, and so does a group nested in it. A group that fits, such as\n`[--json|--plain]`, stays whole. Only a single option with its argument that is wider than the line\nbreaks at its spaces. Examples are not wrapped, so each stays one line to copy.\n\nWhen the flag names leave their descriptions fewer than 30 columns, each description goes on the\nlines under its flag. A report's list of commands follows the same 30-column rule. Below 48 columns,\na record puts each label on a line of its own with its value under it. Output to a pipe or a file is\nnever wrapped.\n\nA read that could not read part of what it was asked for is `partial`: `result` holds what it read,\n`completeness` is `partial`, each part it could not read is a warning, and it exits 4. A list is\nnever presented as complete when part of it is missing.\n\nA target the command cannot resolve or reach is refused before any request, with the code for why:\n`TARGET_NOT_FOUND` (no such mesh), `TARGET_AMBIGUOUS` (several match), `TARGET_UNAVAILABLE` (its\nbroker or manager did not answer), `AUTH_REQUIRED` (it needs a sign-in or a fresh credential),\n`PERMISSION_DENIED` (it refused the credential), `DEPENDENCY_UNAVAILABLE` (trust material, the\nuser-auth service or a space catalog it needs could not be used), or `UNSUPPORTED`.\n\nA command with a JSON output exits by its outcome, with or without `--json`:\n\n| Outcome | Exit | Codes |\n|---|---|---|\n| `ok`, `no-op` | 0 | none |\n| `refused` before any effect, for invalid input | 1 | `USAGE_INVALID`, `SCHEMA_INVALID` |\n| `refused` at a known boundary before any effect | 2 | `PERMISSION_DENIED`, `AUTH_REQUIRED`, `TARGET_UNAVAILABLE`, `TARGET_NOT_FOUND`, `TARGET_AMBIGUOUS`, `DEPENDENCY_UNAVAILABLE`, `UNSUPPORTED`, `CONFLICT`, `LIFECYCLE_BLOCKED` |\n| `unknown`: it may have had an effect that cannot be attributed. Do not retry blindly | 3 | `OPERATION_UNKNOWN` |\n| `partial`: some of the work completed, or cleanup remains | 4 | `OPERATION_PARTIAL` |\n| `failed`: a confirmed failure with no cleanup left | 5 | `OPERATION_FAILED` |\n\nAn error such a command does not classify is `unknown`, exit 3. Other commands still exit 1 on any\nfailure.\n\n## Commands\n\n| Area | Command | Purpose |\n|---|---|---|\n| Set up & lifecycle | [`setup`](#setup) | Prepare a space, application and runtimes; opt into startup separately |\n| Set up & lifecycle | [`update`](#update) | Reconcile first-party extensions and check or opt into a coherent CLI upgrade |\n| Set up & lifecycle | [`up`](#up) | Start a local mesh (nats-server + JetStream), or boot a whole manifest with `-f` |\n| Set up & lifecycle | [`down`](#down) | Stop the whole stack, selected registered components, or a manifest deploy |\n| Set up & lifecycle | [`backup`](#backups) | Create an offline full-space or registry-only artifact from a preserved cut |\n| Set up & lifecycle | [`clean`](#clean) | Configurable cleanup: purge history (live), or wipe the local store / identity (stopped) |\n| Set up & lifecycle | [`reset`](#reset) | Preview or reset one stopped local store or space identity |\n| Set up & lifecycle | [`space`](#selecting-a-space) | List the spaces this machine can reach, or show one in full |\n| Set up & lifecycle | [`use`](#selecting-a-space) | Select the space commands target when none is named, or show the selection |\n| Set up & lifecycle | [`meshes`](#mesh-registry) | List, register and drop meshes; `space` replaces its listing |\n| Set up & lifecycle | [`sync`](#mesh-registry) | Refresh the signed-in account's advertised spaces |\n| Set up & lifecycle | [`status`](#mesh-registry) | Read-only diagnostics for setup, processes, and the selected mesh |\n| Agents & personas | [`spawn`](#spawn) | Launch an agent from a persona (foreground, or `--detach` via the manager) |\n| Agents & personas | [`models`](#models) | List connector model catalogs and variants from the manager |\n| Agents & personas | [`ps`](#managed-seats) | List managed agents and their mesh status |\n| Agents & personas | [`stop`](#managed-seats) | Ask the manager to stop a managed agent |\n| Agents & personas | [`attach`](#managed-seats) | Stream and drive a managed agent's terminal (pty runtime) |\n| Agents & personas | [`input`](#input) | Type one line into a managed agent's terminal without attaching |\n| Agents & personas | [`persona`](#persona) | List, show, edit, create, or remove local personas |\n| Agents & personas | [`supervise`](#supervise) | Deprecated and hidden from help: run a manager daemon; use the `manager` service preset |\n| Agents & personas | [`service`](#service) | Save, start, stop and inspect owned local services, from a preset or a definition file |\n| Agents & personas | [`daemon`](#daemon) | Linux: serve the local machine daemon's control endpoint, read its status, sign it in, check and request its own registration with a space |\n| Agents & personas | [`runtimes`](#runtimes) | List the agent runtimes the manager can spawn through and whether each is reachable |\n| Agents & personas | [`seats`](#seats) | List the pty seat custodians an earlier Linux manager left, and drain the ones whose agent has exited |\n| Agents & personas | [`endpoint deregister`](#endpoint-deregister) | Remove the service registration of an instance whose host is gone, when the broker affirms nothing is subscribed on its rail |\n| Agents & personas | [`reconcile-gate`](#reconcile-gate) | Unfreeze an issuance gate left frozen by a crashed restart when the successor cannot boot-heal it (holder gone, complete CONNZ sweep) |\n| Messaging & watching | [`who`](#who) | The live roster of the selected space, and whether a manager runs each seat |\n| Messaging & watching | [`whoami`](#whoami) | The identity this CLI acts under, and what it may read, post and do |\n| Messaging & watching | [`endpoints`](#endpoints) | List every endpoint in the live presence roster, including infrastructure |\n| Messaging & watching | [`describe` / `invoke`](#endpoint-control) | Resolve a v0.4 service's command surface off the wire; invoke one command by name |\n| Messaging & watching | [`send`](#send) | Send one message, then exit: DM a peer, post a channel, or ask a role |\n| Messaging & watching | [`chat`](#chat) | Open a live session under its own identity: terminal view, `--plain` lines, or `--stdio` for programs |\n| Messaging & watching | [`channels`](#channels) | Inspect or set the channel registry |\n| Messaging & watching | [`history`](#history) | Read retained messages, or clear them |\n| Messaging & watching | [`pending`](#pending) | Inspect one DM durable's native counts and bounded candidate IDs |\n| Messaging & watching | [`console`](#console) | Live protocol view for a space (TUI, or `--plain` line stream) |\n| Messaging & watching | [Web dashboard](#web) | Browser dashboard, run as the `web` service preset (installed as the `@cotal-ai/web` extension) |\n| Extensions & misc | [`linear`](#linear) | Serve the official Linear MCP server as an endpoint agents can call (installed as the `@cotal-ai/linear` extension) |\n| Auth & meshes | [`mint`](#mint) | Mint a creds file for a space (static auth mode) |\n| Auth & meshes | [`login`](#login) | Sign in to a per-user-auth mesh's IdP (once per machine) |\n| Auth & meshes | [`logout`](#login) | Revoke the IdP session and clear the cached login |\n| Auth & meshes | [`actor`](#actor) | Manage a user-auth space's actor ledger (grant / revoke / list) |\n| Auth & meshes | [`doctor`](#doctor) | Credential-health diagnosis and repair (`doctor auth`) |\n| Auth & meshes | [`join`](#join) | Join a space as your own presence (interactive) |\n| Manifest | [`topology`](#manifest-deploys) | Validate and view a mesh manifest's access graph (read-only) |\n| Extensions & misc | [`ext`](#ext) | List, install and remove operator CLI extensions |\n| Extensions & misc | [`docs`](#docs) | Read this version's protocol and CLI docs, offline |\n| Extensions & misc | [`config`](#config) | Show and set your preferences: the defaults your commands use |\n| Extensions & misc | [`completion`](#completion) | Report, print or install shell completion |\n| Extensions & misc | [`feedback`](#feedback) | Send feedback to the Cotal developers |\n| Extensions & misc | [`telemetry`](#telemetry) | Show or set the anonymous usage telemetry level (`off`, `minimal`, `regular`) |\n| Extensions & misc | [`deliver`](#server-daemons) | Deprecated and hidden from help: run the delivery daemon; use the `delivery` service preset |\n| Workflow runs | [`run`](#run) | Operate durable workflow runs: start, resume, list, inspect, answer a checkpoint, check an edited program with migrate |\n| Extensions & misc | [`feedback-intake`](#server-daemons) | Deprecated and hidden from help: run a feedback intake server; use the `feedback-intake` service preset |\n| Extensions & misc | [`auth-service`](#server-daemons) | Deprecated and hidden from help: run a user-auth space's auth service by hand; use the `auth-service` service preset |\n\nThe manifest modes of `up`, `spawn`, and `down` (`-f <cotal.yaml>`) plus `topology` are covered\ntogether under [Manifest deploys](#manifest-deploys).\n\n## setup\n\n```bash\ncotal setup [--destination <local|self-hosted>] [-H|--harness <connector>] [--runtime <name>]... [--project <dir>]\n [--space <name>] [--connection-file <bundle.json>] [--configure-only | --start [-d|--detach]]\n [--on <manager-instance>]\n [--demo|--full] [-y|--yes] [--json|--plain]\ncotal setup --skills\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--destination` | asked | `local`: a space this machine starts. `self-hosted`: a space registered here that runs on a server. `cloud` is refused as in development, before anything is written or contacted. |\n| `--harness`, `-H` | asked | The connector your agents run in. Setup runs its own setup actions and records it as the space's `harness` preference, so `cotal spawn` in the space launches it. When you have no `harness` preference of your own, it becomes yours too. |\n| `--runtime` | asked; none in a script | A runtime to set up besides the built-in `pty`: `tmux`, `cmux`, `orca`, `herdr` or an installed third-party one. Repeat it for each. |\n| `--project` | asked | The project folder: the space's root, whose `.cotal/agents` holds its personas. A script's folder must exist; a terminal offers to create it. |\n| `--space` | asked | The space to prepare (local) or the registered space to use (self-hosted). |\n| `--connection-file` | none | Self-hosted: register the space from a connection bundle exported where it runs, with the checks `space add --file` makes. |\n| `--configure-only` | on | Save preparation and start nothing. Refused with `--start`. |\n| `--start` | off | After successful preparation, start the local stack when needed and launch the default persona. Self-hosted setup uses its registered manager and starts no replacement local stack. |\n| `--on` | none | Select one self-hosted manager instance for startup. Local setup pins the manager it starts and refuses this flag. In a script, use it with `--start`. |\n| `--detach`, `-d` | off | With `--start`, leave the managed agent running without opening a terminal view. Required in scripts, JSON and plain mode. |\n| `--demo` | off | Also seed the guided expert team (`david`, `sven`, `me`) |\n| `--full` | off | The guided flow with the guided team (implies `--demo`) |\n| `--yes`, `-y` | off | Ask nothing: every input comes from the flags |\n| `--skills` | off | Reconcile Cotal skills only through installed connector providers, plus `~/.agents/skills`. Refused with the four inputs, `--runtime`, `--connection-file`, `--configure-only`, `--start`, `--on`, `--detach`, `--full`, `--demo`, `--json` or `--plain`. |\n\nSetup saves preparation before a separate startup decision. Configure-only is the default;\n`--yes` never turns it into startup. On a terminal it asks for each input no flag gave: destination,\napplication, runtimes, project folder and space.\nIt shows the answers before it writes: Save writes them, Change asks every question again with the\nanswers kept, and Cancel saves nothing. Cloud is listed as in development and cannot be chosen.\n\nWithout a terminal, or with `--yes`, `--no-input` or `--json`, every input is a flag. A missing one\nis `USAGE_INVALID` naming each missing flag, before anything is written:\n\n```bash\ncotal setup --destination local --harness claude --project ~/research --space research --no-input\n```\n\nA local space is prepared as `cotal space create` prepares it. A self-hosted space is one already\nregistered with `cotal space add` or a signed-in catalog, whose root must be `--project`, or one\n`--connection-file` registers. Setup then seeds the `default` persona into\n`<project>/.cotal/agents`, the catalog `cotal spawn` reads for that space, records the application as\nthe space's `harness` preference, runs the application's setup actions, and reconciles Cotal's\nskills. When you have no `harness` preference of your own, setup also records the application as\nyours, so a space you prepare later with `space create` uses it. A value you already set stays.\nThe result names the next command: `cotal up` in the project for a local space, or\n`cotal spawn --space <name>`. `--json` writes one envelope whose `result.written` lists every file\nsetup wrote.\n\nSetup needs an installed connector. With none, a `--harness` that names no official connector is\nrefused with `USAGE_INVALID`, exit 1, and otherwise setup refuses with `DEPENDENCY_UNAVAILABLE`,\nexit 2. Its remedy names `cotal ext seed` on a released install. A source checkout does not seed\nconnectors unless `COTAL_ALLOW_CHECKOUT_SEED=1` marks a sandboxed release test, and the remedy says\nso.\n\nAfter successful preparation, the terminal offers Start separately with No selected. An explicit\n`--configure-only` suppresses that question. `--start` skips it; scripts must also pass `--detach`.\nMissing application/runtime prerequisites keep preparation saved and do not launch anything.\n\n```bash\ncotal setup --destination local --harness claude --project <dir> --space <name> --start --detach --json\n```\n\nFor a local space, setup invokes the current native `up` in the prepared project, then pins the\nagent request to that root's recorded manager. Native up returning successfully is not reported as\nwhole-stack health. The managed launch must return its actual joined identity. Self-hosted startup\nuses the registered manager's persona catalog; `--on` pins its manager instance\nthrough credential issuance and launch. It uploads no local persona file. Without detach,\nthe terminal opens that allocated incarnation's native view. If it disappears before attachment,\nsetup reports the confirmed join and an incomplete view, not a healthy attached terminal. Leaving\nthe view sends no stop request.\n\n`result.startup` separates the requested self-hosted manager pin, native stack-command\nreturn, joined-agent receipt and any classified failure. A declined Start keeps the\npin in the suggested spawn command rather than silently selecting another manager. A joined result includes its full manager identity and a usable exact `attach`\ncommand when its returned tokens are safe to put in a shell command. Preparation remains saved\nafter a denied or uncertain start. Unknown outcomes are not retried; the setup heading is not\nshown as healthy and the next command inspects the original startup before attachment. JSON stdout has\none envelope; native startup output is not parsed or echoed as readiness evidence. Fresh prepared\nstatic-auth, open and plain-loopback user-auth roots use the controller stack described under `up`.\nManifest startup retains its native launch path.\n\nRuntimes are where agents run. `pty` is built in and always set up. The terminal lists the official\nruntimes and every installed third-party one, each with whether it is set up and whether the host\ntool it drives is on PATH, or named by the driver's own variable (`COTAL_ORCA_BIN`,\n`CMUX_BUNDLED_CLI_PATH`); installed ones and ones whose tool was found start selected. Setup\ninstalls each selected runtime as `cotal ext add` does, at this cotal's version, then asks its\nprovider whether it answers, as `cotal runtimes` reports. A selected runtime whose host tool is\nmissing is not installed, and one that does not install or answer leaves the rest saved and the\nresult `partial`. A script sets up only the runtimes `--runtime` names; an unknown name is\n`USAGE_INVALID` and a missing host tool is `DEPENDENCY_UNAVAILABLE`, both before anything is\nwritten. Setup never removes a runtime you leave out; `cotal ext remove` does. `result.runtimes`\nlists each one with its package, its host tool and whether setup found it set up, added it or left it\nout.\n\nA rerun with the same answers changes nothing and is a `no-op`; a space already prepared, started or\nregistered at that root is reported, not refused. A space name or root another space holds is\n`CONFLICT`. An application whose host tool is not on PATH leaves the rest saved and the result\n`partial` (exit 4), naming the tool; an application action that does not finish does the same.\n`cotal status` reports the machine's state, and it points\nstale Claude skills and out-of-date `.agents` skills at `cotal setup --skills`. See\n[Getting started](getting-started.md) and, for maintainers, [setup internals](setup-internals.md).\n\n## update\n\n```bash\ncotal update [--self] [--space <s>] [--server <url>] [--creds <path>]\ncotal update --check [-j|--json|--plain]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--self` | off | If a newer release exists, install that exact validated `cotal-ai` version globally and reconcile through the newly installed binary |\n| `--space`, `--server`, `--creds` | resolved mesh | Select the running manager whose continuity state is reported |\n| `--check` | off | Read version availability and recorded extensions without installation or restart; takes no mesh selector or `--self` |\n| `--json`, `-j`, `--plain` | off | Choose the finite check report; requires `--check` |\n\nUpdate reads the recorded mesh inventory without refreshing account space catalogues. An expired\nlogin to a remote mesh does not block the software update. Local manager continuity checks still\napply before changing the installation.\n\nWithout `--self`, `update` keeps the installed first-party surfaces coherent with the running\nbinary: it force-reconciles the four built-in connectors, brings what a connector installed in its\nharness to this release (the `cotal` plugin copy Claude Code sessions load, and its Claude Desktop entry), then reinstalls other `@cotal-ai/*`\noperator extensions at the binary's exact version. Each extension runs in an isolated child, so one\nfailure cannot poison later replays. It then checks npm; a newer binary is an informational notice\nwith `cotal update --self` as the next command, not an automatic install.\n\nBefore disk reconciliation, `update` reads the selected running manager. A machine with no recorded\nmesh has no running manager to observe, so that read is skipped and the command completes. The same\nholds when every recorded mesh is down and none is selected. A remote user mesh, registered with\n`cotal meshes add --mode user`, is named and skipped: its manager runs under another install, so\nthere is no custody on this machine to preserve, and a `legacy` verdict still comes only from a\nmanager this machine read. A stopped mesh that this machine runs, chosen with `--space` or by running\ninside its project, has no running manager either when no `cotal supervise` process on this machine\nmay be serving its space and nothing listens at its recorded broker address, so the read is skipped\nthere too. That is the cold path past a `legacy` verdict: `cotal down`, then `cotal update`. Any\nother selected mesh whose manager cannot be read is still a refusal: a named space that is not\nrecorded, a mesh registered with `cotal meshes add`, a `--server` override, a `cotal supervise`\nprocess still running from any folder whose command line names that space or no space, or a broker\naddress where anything accepts the connection or the dial does not finish in time. With several\nmeshes running and no `--space`, `--server` or `--creds`, the install is machine-wide, so every\nrunning manager is reported in turn, each under its space name, before anything is written; a\n`legacy` verdict on any of them makes the whole run not a hot update. A selector flag still reports\none manager. A manager without a\ncustody generation is reported as `legacy`: it cannot preserve its manager-owned PTYs, so the\ncommand says that this is not a hot update and prints `exact`, `fork`, `fresh`, or `drain-only`\nfor every seat. This report sends no stop, preservation-commit, or replacement command.\nIt does not preserve a running PTY on a legacy manager. The built-in pty runtime spawns\nin-process on every platform and reports `legacy`. On Linux it still adopts seats that an earlier\nmanager left under a detached custodian, but it starts no new custodian. An incompatible native\n`@lydell/node-pty` or ConPTY ABI break remains an explicit per-seat maintenance cut.\n\nWith `--self`, the selected running manager is reported before any global install. When a newer\nrelease exists, Cotal then installs the exact version it validated, resolves and verifies that\npackage in npm's global root, then launches that binary with the same `--space` / `--server` /\n`--creds` selection to reconcile connectors and first-party extensions to the new generation. An npx\nor dev-clone invocation therefore installs and continues through a separate global copy; it never\nclaims the already-running process changed. From a source checkout, the `update --self` hint in\n`update` and `update --check` says so. If the binary is current, `--self` performs the normal\nlocal reconcile without reinstalling it. Each npm install it runs starts with 022 added to your\numask, so a umask of 0002 does not leave the installed tree group-writable.\n\nThird-party extensions are listed with their installed version and recorded spec but are not\nauto-updated in v1. Floating third-party updates require `@cotal-ai/*` peer-range validation and are\na future follow-up. A failed connector/extension install, npm metadata check, or requested global\ninstall is reported and makes the command exit nonzero. Independent extension attempts continue so\nthe output includes every failure; an unavailable npm registry does not undo a completed local\nreconcile, but the command still exits nonzero because it could not establish that the install is\ncurrent.\n\n## up\n\n```bash\ncotal up [--foreground|--detach] [--open] [--space <s>] [--root <prepared-dir>] [--server <url>] [--channels <path>] [--runtime <name>]\ncotal up --user-auth --idp <url> [--exchange-public-port <n> --exchange-public-url <https://\u2026> [--exchange-trusted-proxy]]\n [--advertised-server <url>] [--agent-provisioning-url <https://\u2026>]\ncotal up --tls-cert <cert.pem> --tls-key <key.pem> # serve broker TLS (both, or neither)\ncotal up --restore <dir> [--restore-only registry] [--accept-missing-source]\ncotal up -f <cotal.yaml> [--dry-run] [--runtime <name>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--server <url>` | auto (free local port) | Listen URL override: one `nats://` or `tls://` URL with a host, without credentials, path, query or fragment. Any other value refuses `USAGE_INVALID` before anything is written. A controller stack's broker also needs one IP literal that is not a wildcard, which `up` checks before it writes the stack journal. With `-f`, it overrides the manifest's `broker.servers` and follows that field's rules instead: a valid URL with no embedded credentials |\n| `--host <host>` | none | Bind host override for a **fresh** broker boot: an IP or hostname only, never a URL (that is `--server`) and never `host:port` (the port comes from `--server` or its default); a URL or port-bearing value is refused pointing at the right flag. With no `--server`, the broker URL is derived from it, so `--host <addr>` alone is enough to make a mesh reachable at that address; a `--host`/`--server` pair naming different addresses is refused. A wildcard bind (`0.0.0.0`, `::`) keeps a dialable loopback URL. Recorded on the mesh and reused by every later manager launch, so a repair or resume keeps remote [`attach`](#managed-seats) working. A live refresh (`\u2713 mesh already running`) does not rewrite `.cotal/auth/server.conf` or rebind nats; stop the broker, then re-run `up --host` |\n| `--space <s>` | the folder's space | Space name |\n| `--root <prepared-dir>` | current folder's workspace | Use an existing local project's own `.cotal` root rather than inheriting one from the invoking folder. An absent, inherited or symlinked `.cotal`, or the machine's home store, refuses before startup. Prepare a new folder with `space create <space> --root <dir>` first. Relative file, restore, channels, TLS and store paths remain relative to the invoking folder. The native `up` selection rule still applies after startup. |\n| `--store-dir <dir>` | none | JetStream store directory (recorded; a repair up reuses it) |\n| `--max-file-store <bytes>` | nats-server's dynamic cap | JetStream file storage cap in bytes (a positive integer, no unit suffix). Without it nats-server sizes the store at start as three quarters of the free space on its filesystem. The cap is fixed at broker start: a running broker cannot change it (`cotal down` first), `down --preserve-state` keeps it for the resume, and a resume with a different value is refused. Not accepted with `-f` |\n| `--channels <path>` | `.cotal/channels.json` if present | Channel-registry seed file (JSON). An explicit path that is missing is an error |\n| `--restore <dir>` | none | Restore a completed offline backup before exposing the normal listener |\n| `--restore-only registry` | artifact selection | Restore only the registry component |\n| `--accept-missing-source` | off | Explicit disaster consent when the inode-bound preserved source is absent |\n| `--accept-stale-checkpoint` | off | Explicit consent to resume a seat whose checkpoint was captured outside its recorded recency horizon |\n| `--open` | off (auth) | Unauthenticated dev mesh: no JWT, no ACLs |\n| `--user-auth` | off | Per-user auth: people `cotal login`; connects are authorized against the actor ledger |\n| `--idp <url>` | none | With `--user-auth`: the IdP auth base URL to pin on first enable. A later run may omit it or pass any spelling that normalizes to the pinned URL, such as one with a trailing slash; a different IdP is refused. With `-f`, overrides the manifest's `broker.idp` under the same rules: https (or http on a loopback IP literal), with no credentials, query or fragment |\n| `--exchange-public-port <n>` | none | With `--user-auth`: add the public exchange face on this loopback port, for an HTTPS reverse proxy to forward to |\n| `--exchange-public-url <https://\u2026>` | none | With `--exchange-public-port`: advertise the reverse proxy's HTTPS URL in discovery |\n| `--exchange-trusted-proxy` | off | With `--exchange-public-port`: attribute public failure buckets to the last `X-Forwarded-For` hop. Enable only when the listener is reachable solely through a trusted proxy; otherwise the socket address is used |\n| `--advertised-server <url>` | `--server` | With `--exchange-public-port`: the broker address the public discovery bundle tells participants to dial, such as a `wss://` address behind your proxy. Must be a `nats://`, `tls://`, `ws://` or `wss://` URL |\n| `--agent-provisioning-url <https://\u2026>` | none | With `--exchange-public-port`: the deployment's agent-provisioning endpoint the public bundle advertises. A participant's `cotal spawn` on a remote user-auth mesh sends its login bearer there to provision the agent; without one, that spawn needs a one-time enrollment ([Run a mesh](run-a-mesh.md#remote-supervised-seats-by-enrollment)). HTTPS only |\n| `--service-endpoints` | off | With `--user-auth`: the auth service also hosts service endpoints: the public registration door and the authenticated host route that `cotal service-endpoint` drives |\n| `--detach` | on | Run in the background, which is also the no-flag default |\n| `--foreground` | off | Keep native stack lifetime in this terminal. Required when a systemd/launchd unit supervises `up` itself. Cannot combine with `--detach` |\n| `--tls-cert <path>` | none | PEM certificate to serve TLS with. Must be given together with `--tls-key`. Before starting the broker, Cotal checks readability, private-key mode, key/certificate match, the validity window, and host coverage. `nats-server` accepts an expired certificate and leaves the failure to clients, so Cotal performs these checks first. The decision is recorded; a later bare `cotal up` keeps serving TLS |\n| `--tls-key <path>` | none | PEM private key for `--tls-cert`. Refused if group- or other-readable (tighten to `600`) |\n| `--file <cotal.yaml>`, `-f` | none | Launch a whole mesh from a manifest |\n| `--dry-run` | off | With `-f`: print the plan, mutate nothing. A preview from an unprepared folder under the machine home reads that folder's empty policy rather than inheriting the home store; applying there requires `space create` first. |\n| `--runtime <name>` | `pty` (or the manifest's, with `-f`) | Agent runtime for the mesh manager (`pty` built in; others are installed extensions, explicit-only). Resolved + probed before the broker starts; an uninstalled/unreachable runtime fails loud. Without `-f`, that refusal is `DEPENDENCY_UNAVAILABLE` (exit 2) before anything is written, and for a known runtime the remedy names the `cotal ext add <pkg>` to run. With `-f`, overrides the manifest's runtime |\n| `--max-sessions <n>` | 64 | Live-session ceiling for the mesh manager. Each console pane and each `cotal attach` is one session, so size for agents \xD7 panes, not agent count. Recorded on the mesh and reused by every later manager launch, so a repair or resume does not silently drop back to 64. A running manager cannot change it: `cotal down` first, then `cotal up --max-sessions <n>` |\n| `--no-manager` | off | Broker-only boot: start the broker and, in auth mode, the delivery daemon, and no local manager. A refresh under the flag of a mesh whose manager is live refuses rather than keeping or stopping it. For a controller stack, use `cotal service stop stack-manager --space <s> --yes` first. A legacy stack has no command that stops only its manager: `cotal down` stops the whole stack, then `cotal up --no-manager --space <s>` starts it broker-only. Cannot be combined with `--runtime`, `--max-sessions`, or an agent-declaring manifest |\n| `--rotate-sys` | off | Rotate the space's system account and re-mint its two `$SYS` creds. Needs a stopped mesh; refused with `--open` |\n\n`cotal up` boots a local nats-server with JetStream and, in auth mode (the default), JWT auth and\nper-agent ACLs. It runs in the background by default and records the mesh so `cotal spawn` from\nany directory can find it; `--detach` explicitly names the same mode. `--foreground` keeps the\nnative stack in this terminal. A contradictory mode pair refuses before setup or seed work. An\nunqualified `up` from a folder that inherits the machine's home `.cotal` refuses before seeding:\nprepare that folder with `space create <space> --root .`, then run `up --root .`, or move to the\nhome folder if the home stack was intended. When `--root` names another prepared project, run bare\n`cotal down` from that project folder: bare down\nstops the folder's stack, and `down --space` is not a remote replacement for that scope. With no\n`--space`, `up` runs the folder's space: the one its auth account names, else the one its stack or\nrunning mesh records, else the one prepared there, else `main`. A second `up` in the same folder\nruns the same space again. A folder whose stack and registrations name more than one space refuses\nwith `CONFLICT` and asks for `--space`. With no\n`--server`, it auto-selects a free port if the default address is taken, and a later `cotal up` of\nthe same space from the same folder starts from the URL that mesh recorded; an explicit `--server`\nstays fail-loud on collision. A background boot also brings up the control plane (delivery daemon in auth\nmode, then the manager). `--no-manager` is the broker-only mode: it boots\nthe broker (and the delivery daemon in auth mode) and starts no manager, so there is no manager\npidfile to leave stale. A refresh under the flag of a mesh whose manager is live refuses rather\nthan keeping or stopping it. Stop a controller stack's manager first with\n`cotal service stop stack-manager --space <s> --yes`. A legacy stack has no command that stops only\nits manager: `cotal down` stops the whole stack, then `cotal up --no-manager --space <s>` starts it\nbroker-only. For a split topology with a manager, wait for `.cotal/manager.<spaceKey>.log` to contain `\u2713 manager up`, then stop that manager the same way on that\nhost and run [`supervise`](#supervise) against the remote broker; see\n[Run a mesh](run-a-mesh.md). `cotal up --detach` prints `\u2713 running in the background:` with\n`manager` listed (pidfile liveness, not a teardown boundary); with `--no-manager` the line lists\nonly what actually started. Started from the machine home, the line names `cotal down --preserve-state`,\nbecause bare `down` refuses the home stack. With `--no-manager` it names `cotal up` first, because\n`--preserve-state` needs a manager. On POSIX, a foreground broker has its own session and process group.\nTerminal Ctrl-C reaches the CLI owner, which stops the manager through the same stop as bare\n`cotal down` (see [`down`](#down)), then the rest of the stack, and reports managed agents under\nthe same rule. When the manager stop is refused, Ctrl-C prints the refusal with the reap route and\nleaves the stack running, including the manager, broker, delivery and any auth service. A second\ninterrupt during teardown does not start another stop. Windows keeps its existing console signal\nbehavior. The `-f` form is a\n[manifest deploy](#manifest-deploys).\n\nAn `up` that starts or refreshes a space on this machine writes the `default` persona into\n`<root>/.cotal/agents` when the root has none, so a bare `cotal spawn` has a persona to launch. A\nspace prepared or started before it had one gets it on its next `up`, and a persona you edited is\nleft as it is. A controller stack lists the write under `result.written` and in its report, and the\nretained launcher prints the path it wrote. If the write fails, the stack stays up and the failure\nis a warning. A controller recovery or restore leaves the persona to the next `up`.\n\nA space name belongs to one registered mesh. `up` refuses a name that another live mesh holds. It\ntakes over a name whose broker is not running, and when that registration belonged to another root\nit prints that root and its broker address. It refuses instead when that root holds a restore or a\npreserved cut in its maintenance journal: finish that attempt from its root, or start this stack\nunder another `--space`. It also keeps the name, and names that root, when the root or its journal\ncannot be read, for example when access to a parent folder is denied, or when the recorded root is\nnot an absolute path. A recorded root matches this one only by the folder it resolves to on disk, so\na path such as `<this root>/link/..` whose link leads through an unreadable folder keeps its name\nlike any root that cannot be read. A root counts as gone only when it no longer exists or is not a\ndirectory. A readable root with no maintenance journal also gives up its name. Each of these\nrefusals ends `refused` with exit 2, on a controller stack and on the retained launcher, and shows\nits reason and the next step, such as `cotal meshes rm <space>` for a root that is not an absolute\npath.\n\nA foreground `up` holds the root's maintenance lock only while it starts the stack. A bare\n`cotal down` from another terminal can then stop the stack; the foreground `up` sees the completed\nstop or preserve, prints `Foreground stack released` and exits 0. A systemd unit that runs\n`up --foreground` should set `KillMode=mixed`, so the stop signal reaches only `cotal up`, which runs\nthe guarded stack stop, and a `TimeoutStopSec` long enough for that stop. The default\n`control-group` mode signals every member at the same time, and a manager signalled directly does\nnot stop through its request-bound stop.\n\nThe generated broker configuration is owner-private (`0600`) in open and authenticated modes,\neven with a group-writable umask. `up` creates `.cotal` and the broker store owner-private (`0700`)\ntoo. An existing directory keeps its mode. The broker log `.cotal/nats.log` and the auth service,\ndelivery and manager logs are created owner-private as well, and the process records `up` writes, such as\n`.cotal/nats.pid` and its identity pin, are created `0600` under a fresh name that is never opened\nthrough a symlink, then renamed into place. The pid record and identity pin of a native service\ninstance are published the same way. A file Cotal writes in place under `~/.cotal` or a\nproject's `.cotal`, such as a generated configuration, a credential, a process marker, a persona,\na seat record or one of those logs, is never written through a symlink.\nA file you own is set to `0600` through the open file. A symlink, a file another user owns or a name\nthat is not a regular file is refused and left unchanged. A private directory is set to `0700`\nthrough its path, which follows a symlink; this step does not refuse a symlinked `~/.cotal` or\n`.cotal`.\nOn Windows the owner and symlink checks do not\napply, because POSIX modes are not that platform's protection model; the file is opened with mode\n`0600` and private secrets are locked down by their access list. Local records under `~/.cotal`\nand a project's `.cotal`, such as the stack journal, service records and deployment attempts, use\nthe same model there. A store directory Cotal creates gets the private access list, and a record is\nwritten like a private secret. Reading a record or checking its path does not check an owner, a mode\nor an access list, and does not refuse a link: a symlink or junction is followed. Size limits, the\nrecord schema, the regular-file check and the refusal of a link inside a record store's own\ndirectories still apply. When a service path check refuses a path, the refusal names the option,\nthe path that failed, and its owner and mode. A whole-stack failure shows that sentence as its\nnative detail.\n\nA fresh prepared static-auth, open or plain-loopback user-auth root starts through the service\ncontroller. It records its mode, definitions and full instance IDs in the private\n`.cotal/stack-controller.json` journal. Static-auth startup prepares trust and configuration, starts\nthe broker, initializes native streams, then starts delivery and the optional manager. Open startup\nprepares an unauthenticated broker and streams, then the optional manager; it creates no delivery\nservice or credential. Each member reports its own readiness; the result does not prove agent or\ntask readiness. `--json` returns one\nstack receipt and `--plain` returns a line-oriented report. Partial startup keeps confirmed members\nand reports them; unknown operations are retained and never retried automatically. A failure while\ninitializing streams or recording the broker registration is attributed to the broker, not the next\nunstarted member. The failure target names that stage. Typed JetStream API errors retain their\nnumeric code and status without copying the server's description or changing the unknown outcome.\n\nA journal root always keeps that controller route and its recorded mode and broker address, even\nwhen a completed stop removed its mesh registration. Bare `up` reuses that address; an explicit\nconflicting `--server` or `--open` refuses before effects. Legacy-only options refuse rather than switching launchers.\n`up -f`, recovery and existing legacy roots retain their native paths; those paths refuse the\nnew JSON/plain flags. A legacy root is one with no stack journal that is neither prepared nor\nregistered for the controller, such as a folder an earlier release started and a normal `down` then\nunregistered. Plain `cotal up` there starts it again on its retained launcher, with its identity,\nstore and history. `space create` in such a folder refuses `CONFLICT` and names that `cotal up`.\nA controller `up` checks that the space is prepared or locally started at its root before it writes\ntrust, configuration, a definition, an instance or the journal; a refusal there leaves the folder as\nit was. When a completed stop and `space forget` removed that record,\n`cotal space create <space> --root <root>` prepares the same space at its root again, and `up`\nstarts it from its journal. Schema apply is not supported. Fresh `team boot` uses the controller\nroute described below. Recovery re-entry keeps its recorded mode.\n\nA whole-stack `up` or `down` takes the root's maintenance lock before it reads the journal. While\nanother whole-stack or maintenance command holds it, the command refuses `LIFECYCLE_BLOCKED` and\nnames that command's pid. A crashed member, or an `up` or `down` killed before it recorded its\noutcome, is settled by the next bare `up` or `down` from process evidence. A member whose recorded\nprocess (pid and start time) is gone is recorded as failed, the earlier operation is recorded as\nfailed, and the command continues with an `OPERATION_SETTLED` warning. A member that is still\nrunning is kept: `up` reuses it and `down` stops it. A member whose process cannot be established,\nor one with a stop that was requested and never confirmed, refuses `LIFECYCLE_BLOCKED` and names\n`cotal service status <definition> --instance <id> --space <space>`. A manager stop whose request\nwas published and not confirmed before `down` stopped waiting keeps the manager's record `stopping`\nunder that request, also after its process exits. The next bare `up` or `down` reads that request's\nown receipts first. When the manager recorded the stop as completed and its exact process is gone,\nthe record becomes `stopped` with that receipt, the request's reservation is released once the\n`down` that took it has exited, and the command continues. When the manager's only receipt is a\nrefusal, which it writes when it started no stop, for example because that `down` had already\nexited, the request is removed, its reservation is released, and the record returns to what its\nprocess shows, so `down` stops a running manager and `up` reuses it. Otherwise it refuses\n`LIFECYCLE_BLOCKED` and names the instance and the original request. `down --dry-run` writes\nnothing, so it does not settle such a stop and refuses the same way. `service start`, `restart` and\n`stop` refuse it `LIFECYCLE_BLOCKED`, exit 2, before any readiness probe, stop request or record write.\nThe earlier stop's outcome stays unknown. When the launch's\nrequest file does not read as its own, the refusal reports that it is unknown whether a stop was\npublished. A manager record that an earlier release left `unknown` while its launch still holds a\nstop request is recorded `stopping` under that request when its process is gone. Its exit is never\nrecorded as a crash, and no `up`, `down` or `service` command relaunches it, removes its record or\nremoves the stack's registration. A stop that has neither a completed nor a refused receipt, such\nas one whose manager was killed before it read the request, is cleared only on request:\n`cotal down --abandon-stop <request-id> --yes`, with the request ID the refusal names. It refuses\n`LIFECYCLE_BLOCKED` while the manager or the `down` that sent the request still runs, and\n`TARGET_NOT_FOUND` when no member retains that request; either way nothing changes. Otherwise it\nremoves the request, the stop reservation and a `--with-agents` shutdown intent that the same stop\narmed, records the manager `failed`, and stops the rest of the stack with a `STOP_ABANDONED` warning.\nWhether that manager stopped or released its agents' seats is not confirmed, and a seat that runs\noutside the manager process may still run. It needs `--yes` and refuses `--dry-run` and\n`--with-agents`. When `up`\nredefines a member because its launch options changed, the journal records the\nreplacement's instance and launch request before the old instance is retired, and the new\ndefinition's revision before the replacement launches. After an `up` cut off in between, the next\nbare `up` or `down` adopts the replacement only when its record carries that launch request and\nrevision for the same operation; `down` stops it and `up` reuses it. Any other instance record\nrefuses `CONFLICT`, and nothing is adopted. A preserve or resume that\nnever recorded its outcome stays with its own maintenance attempt. A member that exits before\nreadiness is recorded as failed with its exit code, and the refusal names its log with\n`cotal service logs <definition> --instance <id> --space <space>`.\n\nBefore it launches a broker, the controller binds the recorded broker address once. When another\nprocess holds it, `up` refuses `CONFLICT`, names the address and the bind error, and launches\nnothing; the stack keeps its recorded address. A text `up` prints one `Starting <definition>` line\non stderr before each member it starts, with the readiness timeout it waits for. A started stack's\nNext offers `cotal spawn`, or `cotal up` to add the manager after `--no-manager`, and `cotal down`.\n\nOn a controller root, `down --preserve-state` refuses `UNSUPPORTED` (exit 2) in this release, in\nevery stack mode. It refuses while validating its arguments, before the confirmation and before any\nstop, write or manager request. Plain `down` stops the stack and keeps its store, channels and\nhistory for the next `up`. Preserving a controller-owned stack returns in a later release.\n`backup create`, `backup restore` and `up --restore` need a preserved cut, so they refuse the same\nway on a controller root (see [Backups](#backups)).\n\nUser-auth controller startup requires a plain loopback IP and an auth provider with read-only\nserver preflight. A first enable checks the required IdP and its JWKS before creating maintenance\nstate, a stack journal, trust or credentials. Missing or invalid inputs are usage refusals; an\nunavailable JWKS is a dependency refusal. Malformed IdP inputs are not echoed. An explicit `--idp`\nis also compared with the recorded pin before maintenance state changes, even while the broker is\nrunning. A different IdP refuses without refreshing members or rewriting their journal. The controller\nchecks again under its lock, and the provider repeats its authoritative checks during preparation,\nbefore any member launches. Later races or failures retain their records and are not rolled back.\nIt starts the broker, initializes streams, records provisional public trust metadata, then starts\nthe `stack-auth` service. Only that exact process's provider readiness supplies the final endpoints.\nDelivery uses the same hosting-root authority as native startup. No user caller is replaced by a\nstatic or borrowed caller.\n\nThe manager requires an existing signed-in, granted CLI login. First enable can therefore start\nthe other members and return partial `OPERATION_PARTIAL` with native cause `AUTH_REQUIRED`, while\nleaving the manager unattempted. Sign in with `cotal login`, obtain the required grant, then run\nbare `cotal up` to continue that known partial operation. `--no-manager` omits that member and its\nlogin requirement. Unknown outcomes are never continued or replayed. Public exchange faces and\nTLS-required or remote user-auth brokers still require the retained native route.\n\nThe journal reads earlier three-member auth/open records with an unattempted auth-service row.\nIts next write records all four roles. It neither adopts legacy processes nor converts a mode.\n\nA repair `up` on a mesh whose broker died reopens the store its record names, and refuses a\ndifferent `--store-dir` rather than silently opening a second store.\n\nThe generated `.cotal/auth/server.conf` is written on a real broker boot and is not an\noperator-owned config. `--host` changes that file only when nats is actually started. A unit\nrestart that leaves an answering listener in place is a refresh, not a rebind.\n\nOn an existing mesh, `cotal up` reconciles the presence and lease bucket TTLs. It writes a reserved\ncanary and waits for the bucket to expire it before reporting success. If the broker accepts the\nstream update but the backing store does not persist or enforce it, `up` exits nonzero with a TTL\npersistence error instead of trusting the value returned by stream info. A refresh that restores a\nmissing manager waits for that process to come up, then says so with its pid (`\u2713 restored in the\nbackground: manager (pid N)`). A manager that `up` starts and that exits at boot instead is reported\nas `! control plane degraded:` with the path of its log, and `up` then exits nonzero, whether it was a\nrefresh, a first `--detach` boot or `up -f`. The same happens when another\n`cotal supervise` takes over the manager pidfile before the started process comes up. A legacy\nrefresh that finds everything already running prints only the `\u2713 mesh \"<space>\" already running`\nline. A controller refresh prints its members under `Controller stack already running`, and its JSON\nresult is a no-op unless it wrote the default persona. Neither lists a role the stack's mode does not use, such as auth on a\nstatic-auth stack. A first\nboot starts its manager without the restore line.\n\nA manager or delivery daemon that is killed, or a daemon that exits because its broker went away,\ncannot release its lease. The row stays until the bucket TTL removes it: 10s for the manager and\n30s for delivery. A repair `up` inside that window waits out the row when it can show the holder is\ngone, then starts the replacement, so one run restores the control plane. For the manager, the row\nmust name this workspace root and a pid that is not running on this host; the new manager logs\n`which is not running on this host; waiting` and the seconds left. For delivery, no daemon may be\nrecorded running in this folder and the row must be held by the credential this folder gave its\nprevious daemon; `up` prints `shard 0 is still leased to this folder's previous delivery daemon`.\nThe wait never writes the lease. A row that is written again while `up` waits has a live holder, so\nthe start is refused and `up` exits nonzero. A row held by any other process is refused at once,\nas before.\n\nA delivery daemon that `up` launched and that exits before it is ready fails the start. The error\nquotes the failure the daemon logged during this launch and names its log,\n`.cotal/delivery.<spaceKey>.log`. It describes a lease held elsewhere only when the daemon logged\nno failure. A daemon start that fails on a root whose maintenance journal holds a restore or a\npreserved cut never suggests `clean store`, because that store holds the restored or preserved\ndata. Check the root with `cotal status` first.\n\nA full restore recreates the delivery daemon's durables from their checkpoints, at their recorded\nstart sequence. The daemon binds such a restored durable when it otherwise matches its own\nconfiguration, so a restored store starts. A durable that differs in any other setting still\nrefuses.\n\n\n`--user-auth --idp <url>` starts the space's auth service alongside the broker: the NATS\nauth callout plus its capability-gated local exchange, and optionally the closed public exchange\nface configured by the three `--exchange-*` flags above. The service is torn down with `cotal down`,\nand a re-run of `cotal up` heals a dead service on a running broker. `up` waits for the service to\nfinish binding: while the daemon it launched (or found running) stays alive, the wait extends past\nthe base 15s up to 60s; a daemon that exits is refused at once with \"exited before becoming ready\",\nand one alive past 60s is refused as \"alive and still starting\" (wedged), naming the pid record and\nthe service log. `--user-auth` and `--open`\ncontradict each other and are refused loudly; a running broker cannot change auth mode\nwithout a `cotal down` first. See [identity & auth](identity-and-auth.md).\n\n`--rotate-sys` renews the two `$SYS` credentials (`membership-observer`, `connection-evictor`).\nThey carry a 30-day expiry and nothing re-signs them in place, because the trust record never\ncarries the system-account seed, so they are renewed by issuing a **new system account** under the\nsame broker operator and minting fresh creds against it. The root keeps that seed in its own secret\nstore, written at space creation and replaced here, so an auth service started with\n`--service-endpoints` can mint the short-lived observer credentials service caller issuance needs. A plain re-`up` does **not** do this: it reuses the\nexisting trust record, and its `$SYS` creds along with it.\n\nThe rotation is safe to run on a real space, with one operational cost. The data account, the account\nsigning key, every agent credential minted from it, and the JetStream store are all untouched; what\ndies is the retired system account, and with it any out-of-band copy of the old `$SYS` creds, on every\nbroker that loads the rotated config. The cost is that **earlier full backups stop being restorable**\n(see below), so this is not a no-consequence operation. It needs the broker to restart on the rewritten\nconfig, so it runs as part of a boot:\n\n```bash\ncotal down\ncotal up --rotate-sys --detach # agents reconnect; nothing is re-provisioned\ncotal doctor auth # both $SYS creds healthy again, 30 days out\n```\n\nA rotation is a stopped, fresh boot, and anything that is not one refuses it, all for the same reason\n(the on-disk material and the broker it runs on must never end up on different generations):\n\n- a live mesh, because the running broker would keep serving the retired account;\n- an open mesh, whether that comes from `--open` or from `broker.auth: false` in a manifest, which\n has no system account at all;\n- `--restore`, because reinstating a trust root and superseding it in one command leaves no way to\n say which authority the mesh came up on;\n- an unfinished restore or resume attempt on this root, including one `cotal up` would recover on\n its own, because those paths can adopt a live listener and return without booting a broker;\n- a root that hosts more than one space, because the system account lives in the shared broker\n record and a rotation would retire every tenant's, while the root holds one `$SYS` cred pair\n pinned to one data account.\n\nTwo things to know before you run it:\n\n- **The retirement is config-load-bound.** Old `$SYS` creds are refused by any broker that loads the\n rotated config. A stale `nats-server` still running the *previous* config in memory would keep\n honouring them, so stop every broker for this root first. `--rotate-sys` refuses if this root's\n mesh is recorded as running, if anything unidentified is answering at the address it was given, or\n if the root's pid file names a live (or unreadable) process. Those are Cotal's own ownership\n records, not a scan of the process table: a `nats-server` you started by hand against this root's\n `server.conf` on some other port writes none of them and will not be seen. Do not run one.\n- **It invalidates earlier full backups.** A full artifact binds to the trust chain it was taken\n against, and that commitment covers the operator JWT and the system account. Every full backup\n taken before a rotation refuses to restore afterwards, so take a fresh `cotal backup` once the\n rotated mesh is up. `cotal up --restore` names this case when the data account still matches.\n\nThe commit is not atomic (a trust-record write plus two credential writes), so an interrupted\nrotation leaves the record ahead of the creds. That split is detected rather than silent: every\n`cotal up` on an auth mesh, and every `cotal doctor auth`, compares each `$SYS` cred's issuer against\nthe persisted record and names the retired account. `up` warns rather than refusing, because these\ncreds power the membership graph and live eviction, both of which degrade fail-soft; the mesh is not\nworth taking down over them. Re-running the rotation heals it, at the cost of one generation.\n\nWhile those creds are expired the mesh keeps delivering messages, but the\n[membership feed](delivery-daemon.md) and live connection eviction stay down; `cotal doctor auth`\nand the manager's log both name the credential and this repair.\n\n## down\n\n`down <service-name>` routes one saved owned definition through the\nsame selection, confirmation, reservation, native manager-impact and stop controller as\n`service stop`. Use `--instance <full-id>` when selection needs a full instance pin. `--yes`,\n`--dry-run`, `--space`, `--json` and `--plain` keep that controller's meanings; structured receipts\nname `down` while retaining the service-stop result and actual identity. Unknown definitions do not\nfall back to legacy component pidfiles. A controller stack names its members `stack-broker`,\n`stack-auth`, `stack-delivery` and `stack-manager`; a legacy name such as `manager` refuses\n`TARGET_NOT_FOUND` there and names the `stack-` member to use. Completion offers saved definitions without imports or a\nnetwork request. No hidden connector repair or shared catalog preparation precedes this route.\n\nMultiple names resolve as one selected-space set. All owned targets and process roles are checked\nbefore effects. After confirmation, the controller reserves every definition in stable order and\nrechecks all records and native impact before the first stop. Stops follow the displayed input\norder, with each native guard rechecking at effect. A later failure stops the sequence; one receipt\nretains completed, failed/unknown and unattempted members. Reservation/preflight refusals also\nretain the full selected set. Release is attempted for every acquired definition hold; a reported\nrelease error makes an otherwise known outcome partial without erasing known stop results. An\nalready unknown stop stays unknown. Lock cleanup is not independently proved. This is not atomic and no rollback or cleanup is claimed. Dry-run reads metadata without reservations, provider imports or native impact\nqueries. Duplicate names and `--instance` with multiple names are usage errors. Named `--with-agents` refuses and points to\n`cotal service stop <manager-name> --instance <full-id> --with-agents`; it is never dropped.\n\nBare `down --dry-run` reads local process records for one registered space at the current\nproject root. A root with no `.cotal/` directory, or with no stack registered at it, refuses\n`TARGET_NOT_FOUND`, exit 2, because there is no stack there to preview. A root whose stack is\nalready stopped shows the no-op described below instead. Add `--space <name>` to\nconfirm that exact space belongs to this root;\n`--json` and `--plain` produce a partial, finite inspection receipt. It lists pidfile\nliveness, readable additive deployment IDs, retained additive attempts and\nunreadable paths under this root, and saved service definitions, instance IDs, recorded states and\nhosts, supervisor receipts, and retained install, uninstall and unit-action\nattempts in the selected space's workstation store. Saved records are not attributed to this project root, and their recorded\nstates do not prove live process or supervisor status. A submitted native attempt may have\nchanged state; the preview neither reconciles nor retries it. For each saved service instance it\nalso shows what bare `down` would do with it: stop it, skip it as already stopped, or refuse, with\nthe reason. That plan observes processes on this host and changes nothing. Other local registrations with\nthe exact recorded broker address appear as possible sharing. No match does not exclude\naddress aliases, remote tenants or live attachments. The preview does not establish process\nownership, the complete service set, exclusive broker use or agent impact.\n`--with-agents` only marks the requested policy. It does not reserve or signal\na process, and the receipt is not an authorized stop plan. A fresh `team show`\nsummary points to this read-only command from the recorded root.\n\nOn a root with a controller journal, bare `down` reads only its recorded full instance IDs. It\nshows the member set and the live manager's native agent impact before confirmation, then stops\nmanager, delivery and broker in that order. `--yes` acknowledges that scope in scripts; `--with-agents`\nrequests the manager's native retirement policy. The broker scope is checked again after its\ndependants stop. A failure stops the sequence and preserves completed and unattempted members.\nOnly a complete confirmed stop removes the delivery credential and local registration. Saved\ndefinitions and the journal remain for the next `up`. No descendant, lease or authority cleanup is\nimplied. A dry run reads records only, takes no lock, does not establish native impact and exits 0;\nits JSON marks `completeness` as `partial`. It lists members in stop order, says when the stop would\nfirst settle an earlier operation, and names a scope check the real stop would refuse as a warning.\nA member whose recorded process is already gone is recorded as stopped without loading its adapter\nor credential, so a second `down --yes` stops nothing and is a no-op. When the manager is\nlive but the broker is not, `down` cannot read the manager's agent impact and refuses before any\nstop, from a managed seat's shell too. Run `cotal up --space <space>` to restart the broker from its recorded inputs while the\nmanager keeps running, then run `cotal down`. Controller roots refuse\npreservation and manifest flags rather than switching to the legacy launcher. Outside a terminal,\na bare `down` that would stop a running member refuses without `--yes`, exit 1, and prints that\nline and its remedy without the command help.\n\nA bare `down` in a folder whose stack is already stopped stops nothing. It reports that the stack\nin that folder is not running and names `cotal up`, outcome `no-op`, exit 0, and writes nothing,\nso it needs no `--yes`. `--dry-run`, `--json` and `--plain` show the same. A controller stack\ncounts as stopped when its last whole-stack operation was a completed `down` and no member and no\nother service instance of its space runs. That check reads no process record under the root; those\nare read only as described below. A stack that stopped any other way takes the full stop, which\nalso finishes its cleanup. A legacy root counts as\nstopped when no registration names it and no `*.pid` record remains at the top of its `.cotal`,\nand the root still holds the broker configuration a start rendered there or a space prepared there.\nWithout that evidence a legacy root still refuses, with one next step: `TARGET_NOT_FOUND` when\nnothing shows a stack there, and `CONFLICT` when several registrations name the root or a process\nrecord remains that no registration explains. Before either kind of root reports the no-op, `down`\nreads every record a local-process descriptor names under the root, an installed extension's\nincluded, at any path and name under `.cotal`. A descriptor whose record name holds a space is read\nfor every space its files under the root name, as well as the stack's own. A record whose process\nruns, whose liveness the kernel does not answer, or that cannot be read or attributed refuses\n`LIFECYCLE_BLOCKED`, and so does a file whose space part decodes to no space. Nothing outside the\nroot's `.cotal` is read: a link anywhere on a record's path refuses at the link, unread, as does a\nrecord that is not a regular file, is larger than a process record, cannot be opened, or changes\nwhile it is read. Each is named once, at the path where reading stopped. A `.cotal` that is itself\na link never gives the no-op: on a stopped controller stack `down` refuses at `.cotal`, and on a\nlegacy root it refuses `CONFLICT` before any record is read. A template whose `{space}`\nnormalizes away names one path for every space and is read as that path. The refusal lists each\nrecord by name, path under the root and pid, and its next step names the pids and paths to check.\nIt stops, adopts and removes nothing. `--with-agents` on a stopped stack refuses\n`LIFECYCLE_BLOCKED`, because no running manager can stop the agents; start the stack first. Its\npreview, `cotal down --dry-run --with-agents`, shows the no-op.\n\nThe legacy bare-stack and manifest apply paths described below are retained. They do not\naccept the new named-service JSON/plain/yes apply receipt flags, except that `--json` and\n`--plain` show the no-op of a stack that is already stopped. A bare stop of such a root needs\nno `--yes`, so `cotal down --yes` there is refused and stops nothing; run `cotal down` without it.\nBefore a retained bare apply\nreaches its pidfile sweep, it now requires one locally started registration under\na real isolated project root and refuses pending maintenance transitions, any valid or\nunreadable additive ledger or attempt under that root, or any other local registration at\nthe exact recorded broker address. It reads each record its sweep stops as the stopped-stack check\nabove does. A record that check leaves unread, such as one behind a link, refuses `LIFECYCLE_BLOCKED`\nbefore the first stop and is named once, where reading stopped. No process behind it is read or\nsignalled, and no record is removed. These are known-blocker\nchecks, not a complete live broker-sharing or stop plan; aliases, remote tenants,\nagent impact and process identity remain unproved. These paths do not check full\nwhole-stack target ownership, set exclusion, confirmed agent impact or schema\nreconciliation. The retained `down --preserve-state`\nroute separately refuses a retained or unreadable additive record under its\nmaintenance lock, without adopting its effects into the preserved set.\nA validated completed restore in `active` for the registered space does not block ordinary\nbare `down`. The other root, ownership, sharing and native process guards still apply, and\nstopping leaves the restore journal and fallback store unchanged.\nThe machine home is not an isolated project root, so bare `down` refuses the home stack. Stop it\nwith `cotal down --preserve-state`, which keeps its state for the next `cotal up`. The refusal names\nthat command. `--preserve-state` takes its agent inventory from the manager that serves the space.\nWhen no manager holds the space's lease, as on a stack started with `--no-manager`, it refuses\n`TARGET_UNAVAILABLE`, exit 2, before it stops anything or writes a maintenance record. Start the\nmanager with `cotal up`, then run `cotal down --preserve-state` again.\n\nAfter those checks, a retained bare apply stops the selected space's service instances before its\npidfile sweep. Each goes through the service controller with the checks and receipt of\n`cotal service stop <name> --instance <id> --yes`, and saved definitions stay. An instance whose\nprocess is already gone is skipped. Every instance is checked before the first stop, and one that\ncannot be verified refuses with no service stopped and the broker still running. An unreadable\nservice record keeps the code of its read refusal. A retained native attempt, an unsettled request,\na process that cannot be observed, or a record from another host, which bare `down` does not touch,\nrefuses `LIFECYCLE_BLOCKED`, exit 2. The remedy names that service and the command to run. A stop that does not confirm ends the sequence with the\nstack still running, and completed stops are not rolled back. `--with-agents` refuses when a\nservice would be stopped, because the legacy reap does not reach a service's agents.\n\n\n```bash\ncotal down\ncotal down --with-agents\ncotal down --abandon-stop <request-id> --yes\ncotal down --preserve-state [--store-dir <dir>] [--session-store <dir> \u2026]\ncotal down --dry-run [--space <name>] [--json|--plain]\ncotal down <service-name> [<service-name> \u2026] [--instance <id>] [--space <s>] [--yes|--dry-run] [--json|--plain]\ncotal down -f <cotal.yaml> | --run <id> [--dry-run]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--file <cotal.yaml>`, `-f` | none | Tear down this manifest's deploy |\n| `--run <id>` | none | Tear down one additive ledger, written by the retired `spawn -f` deploy, by its run id |\n| `--space <name>` | current space | With service names: the space whose saved definitions to stop. With bare `--dry-run`: confirm that space belongs to this root. A bare stop refuses it |\n| `--dry-run` | off | Print the manifest teardown, the named service stop or the bare stack preview, mutate nothing |\n| `--with-agents` | off | Bare whole stack only: also stop and deprovision every managed agent |\n| `--abandon-stop <request-id>` | none | Controller stack, with `--yes`: abandon a member's stop that no receipt settles, then stop the stack. See the settlement rules under [up](#up) |\n| `--preserve-state` | off | Bare whole stack only: fence the manager, retain principals and durable state, stop and prove the stack down, then publish `ready` |\n| `--store-dir <dir>` | `.cotal/nats` | With `--preserve-state`: the actual store path (required for a custom store) |\n| `--session-store <dir>` | none | With `--preserve-state`: a harness transcript store directory to capture with every continuation-capable retained seat. Repeatable. No default and never inferred from a connector name; a path that does not exist or is not a directory is refused before anything stops |\n\nBare `cotal down` stops the whole local stack in dependency order and leaves managed agents running\nwhen their runtime lets them outlive the manager. Before signalling the manager it verifies the spare\ncapability of the exact recorded manager, which records what that manager's stop does with its\nseats, and it reports the agents left behind plus `cotal down --with-agents` as the explicit reap.\nWhen the manager had no managed agents, it prints no report. When they cannot be listed, for\nexample on a user-auth mesh with no login on this machine, it prints\n`could not list managed agents (<reason>)` and the stop continues.\nThe built-in pty runtime keeps each PTY inside the manager process, so those seats cannot outlive\nit: every manager stop stops and deprovisions them, and `down` reports them as stopped. Every manager\nstop the CLI makes runs this one path: `down`, Ctrl-C on a foreground `cotal up`, the teardown after\nthat `up`'s broker exits, the leftover-manager stop before `cotal up -f`, and the delivery cutover.\nEach holds the manager's stop reservation, so a second stop while one is in flight is refused,\nnames the process holding it, and leaves that stop's `--with-agents` policy in place. On Windows,\nmanager and delivery stops send an authenticated local cleanup request because Windows process\nsignals terminate without running cleanup handlers. The request requires the process's private\ncapability, its creation identity and the live stopper's identity and reservation. Other platforms\nuse `SIGTERM`. A `cotal supervise` that receives `SIGTERM` or `SIGINT` while it is still starting\nlets the start settle, then runs the same stop and exits without printing `manager up`, so its\nliveness lease is released and an immediate restart is not refused. When that start fails, the stop\nstill releases the lease before the process prints the start error and exits 1. A process still running after 20s on the Windows cooperative path, or 15s after\n`SIGTERM`, receives `SIGKILL`. Ctrl-C stops the manager first; when that\nstop is refused or the manager's exit cannot be confirmed, Ctrl-C signals nothing else, prints the\nrefusal with the reap route, and leaves the stack running; end it with `cotal down --with-agents`.\n`--with-agents` is a one-shot destructive policy bound to the exact verified manager process\nand the exact live `down` stop reservation; a stale, malformed, crashed, or different stop attempt\ncannot turn a later bare shutdown destructive. If a managed agent cannot be proven stopped within\nthe manager's stop timeout, the manager logs which one, still closes its broker connections and\nconsole listener, and exits with code 1. It does not release its pidfile, liveness lease or\nservice registration in that case, so no successor is handed authority while that agent may still\nrun; the lease lapses on its TTL. A named `down` is the service stop above and never stops a\nlegacy component by its pidfile, so `cotal down manager` or `cotal down web` on a legacy stack is\nrefused unless a saved definition has that name. Only bare `cotal down` stops a legacy root's\ncomponents, all together, under the folder you run it in. It takes no `--space`. The\n`-f` / `--run` forms tear down a [manifest deploy](#manifest-deploys) without stopping the whole mesh\nand cannot be combined with service names or `--space`.\n\nA pinned manager with no spare-capability record is not signalled by bare `cotal down`. A current\nmanager always publishes the record, so a missing one means an older\nmanager: one that predates capability reporting, or one whose pty runtime reported that it cannot\ndetach its agents. Stop each managed agent explicitly, then run `cotal down --with-agents` from the\nmesh root to stop the whole stack. An older manager does not understand\nthe reap request, which is why the agents must already be stopped.\n\nAfter the known-blocker admission checks above, the retained bare `cotal down`\ninventories by pidfile. When this folder's registered broker answers and no\n`nats.pid` records it, the command does not say nothing is running. It names the space and the\nbroker address, says no pidfile records that process, says it will not stop a process it did not\nstart, and exits 1. Stop that broker with whatever started it (an init unit, a container, or the\nhand-run process). `cotal meshes rm <space>` only drops the registration. The probe runs whether or\nnot other owned components were running: they stop and clear their artifacts first, then the broker\nis named. A `--dry-run` does not run this probe.\n\n`down` reads each process record once. A component that exits and removes its own record while\n`down` runs counts as having no record, so the stop goes on. Any other failed read is an error.\nWhole-stack shutdown keeps delivery credentials until every required component has stopped.\nA refused stop preserves both the space-scoped credential and its legacy copy. Credential removal\nerrors remain failures. `down` removes the manager's delivery-aware marker, shutdown intent and\nspare capability only after every component has stopped, so a refused stop keeps them too.\n\n**Teardown verifies pinned process identity before signalling.** PIDs are recycled by every OS,\nso a recorded pid alone is not a durable target identity. `up` and the dashboard's native child record each\nprocess's creation identity in a sibling `<pidfile>.identity` pin, which holds the pid and the\nprocess start reported by the OS. Every stop path, including `down` for the broker, web and\nextension components, and the manager, delivery and auth-service stops, applies the same rule. A pin\nthat names a different start means the pid was reused, so teardown refuses and preserves it. A torn\nor unreadable pin also refuses. macOS start tokens keep their spaces, including the padded day,\nin both the identity pin and the one-shot shutdown intent. A single record cannot span lines.\nOn Windows, the reader calls `GetProcessTimes` through a bundled Node-API binding for x64 or\narm64. It requests limited process-query access and preserves the creation FILETIME as a decimal\nstring. It launches no subprocess. Access denial leaves identity unverified; an unreadable pin\nstill refuses teardown. A missing or damaged binding is an installation error and has no fallback.\n\nThe pidfile and its pin are published by renames, and the pidfile rename is the commit point. Just\nbefore it, the pin holds two lines: the old process's and the new one's. A launcher that dies\nmid-publish therefore leaves the old record or the new one, each checked against its own pin line,\nnever a pidfile without its pin. An old record with no pin is legacy, so its line holds `-` in place\nof the token and it stays legacy until the commit. A CLI older than this change reads a two-line pin\nas torn and refuses.\n\nPublishes of one pidfile are serialized by a lock file beside it, `<pidfile>.publish.lock`, because\nthe launcher and the daemon it starts both publish the same record. The next publisher reclaims a\nlock left by a crashed one. When no start token can be read for the new process, its pin line holds\n`-` in place of the token, which reads as a legacy record, and the publish ends in the legacy shape:\na pidfile with no pin. Teardown, and a daemon removing its own record on exit, take the same lock and\nremove the record only while the pidfile still names the pid they stopped, so a stop that races a\npublish leaves the new record whole.\n\nThe web dashboard service claims `web.pid` with an exclusive create, so a second dashboard for the\nsame mesh is refused, and writes its pin right after the claim. A stop that runs between the two\nreads a legacy record. Any existing `web.pid` refuses the start, whether or not its process still\nruns, and the start neither adopts nor reclaims it.\n\nThe pidfile pid and the pin pid are two coordinates. Automatic cleanup follows **proven death of\nthe pidfile target** (ESRCH on that pid): a torn sibling pin does not wedge a dead pidfile pid.\nA torn pairing where the pin names another pid, while the pidfile pid is still live or not proven\ndead, still refuses. Inspect both pids with `ps`. Do not delete `<pidfile>.identity` to force a\nstop; that weakens target-identity protection. Once the pidfile process is dead, rerunning\nteardown clears the stale record automatically.\n\nA Windows manager or delivery daemon without the cooperative capability must be stopped through\nits original supervisor before restarting with this version. Teardown refuses it without signalling\nor deleting its live record. It does not wait for a lease to expire or delete a lease by local pid.\n\nFor components using signals, the first teardown after upgrading a running pre-pin stack has a\nnarrower guarantee. A live record with no identity pin is signalled after a loud warning that it predates identity pinning. Restarting\nthe component writes the pin, so later teardowns receive full match and mismatch protection. The\nsame warning applies on platforms where no stable start token is available. For a legacy manager,\nbare `cotal down` also warns that agent sparing cannot be verified before it signals. Because the\nCLI cannot establish which SIGTERM handler that already-running binary carries, it never presents\nthe pre-signal seat inventory as confirmed spared; a genuinely older destructive handler may still\nreap those agents. `--with-agents` publishes a one-shot reduced-guarantee handoff bound to the\nrecorded manager pid and the live `.stopping` reservation's inode, then signals unconditionally.\nThat handoff cannot be replayed by a later stop attempt. A pin that exists and does not match the\nlive process still refuses before signal.\n\n`--with-agents` performs the old destructive logical teardown: managed processes stop and their\ncredentials, ACL rows, and delivery footprints are deprovisioned. `--preserve-state` is a different\nmaintenance transition: it stops retained processes while suppressing leave/deprovision cleanup, persists the manager's\nsame-principal resume inventory, stops the entire stack without removing run/auth artifacts, and\npublishes a stable inode-bound cut only after every recorded process is proven stopped and the exact\nrecorded NATS endpoint is unreachable. A missing or stale broker pidfile never counts as stopped. The\nattempt is bound durably before the manager is fenced, the resume document and attempt-bound\n`cut-intent` are fsynced before manager commit, and the manager's commitment itself is journaled\n(`cut-committed`) before any process stops. A retry after a crash at any of those boundaries reuses\nthe exact recorded attempt and finishes the remaining stop and endpoint proofs idempotently, without\nneeding the (by then intentionally dead) manager. A partial cut never publishes `ready`. It cannot\nbe combined with component names, manifest teardown, or `--dry-run`.\n\nOn a mesh that runs the delivery daemon, the cut stops it first so no join or leave can change\nmembership under the manager's inventory. Before any manager-owned endpoint stops, the cut then\nreads the presence roster and refuses while an endpoint the manager does not own is live. A refusal\nleaves the delivery daemon stopped. A deleted presence key is the only row it reads as absent. Any\nother row that is not a card with an `id` equal to the presence key it was read under and a string\n`name` refuses the cut with `presence record <subject> is malformed`. A participant can write any\nvalue under its own key, so the key is the identity the cut trusts.\n\n**Seat checkpoints.** After the stack is proven down, the cut writes one checkpoint per retained\nseat under `.cotal/maintenance/v1/checkpoints/<attempt>/<seat>/`, and prints the path, the\ncontinuity class and the generation for each. The path carries the preservation attempt because a\ncheckpoint is immutable once sealed: a shared directory would make the second cut in a root refuse\non the first cut's leftovers, and clearing it would destroy an artifact a rollback still needs. The\ncapture happens only at that point because anything earlier races a harness that is still writing\nits transcript and its working tree.\n\nEach checkpoint directory is created 0700, refuses a destination that already exists, and holds:\n\n- `repo.bundle`, the seat `cwd`'s reachable history, anchored on the base commit the record names\n by full object id;\n- `repo.index.diff` and `repo.worktree.diff`, the staging state as two diffs, base to index and\n index to worktree. Two rather than one because a single combined diff restores a mixed tree with\n the right bytes and the wrong index: a source reporting `MM README` would come back as ` M README`;\n- `repo.untracked.tar`, the untracked files in scope;\n- the harness session pointer, when the seat's connector declares one, and the transcript store\n files the operator named with `--session-store`. Each records where the destination puts it back\n as an anchor (the workspace root, the account home, or the seat's `cwd`) plus a relative path,\n because the destination's root and home are its own and the source host's absolute spelling would\n either miss them or write outside them;\n- `checkpoint.json`, written last, after every digest is computed over the bytes that landed.\n\nThe record carries the manager's resume entry unchanged as its first field, then the space, the seat\nname, the recovered `lifecycleUid`, the writer generation the cut was taken at, `capturedAt`, the\nrecency horizon, the applied profile revision, the seat's `git status --porcelain` as the cut read\nit, and the continuity class. Every captured file is\nlisted with its byte size and sha256, so an operator verifies the whole artifact with `sha256sum`\nand `git bundle verify`. No secret values, no operator keys and no source-host launch material\nenter it.\n\nThe generation is the highest one this root has claimed for the seat in `.cotal/auth`. A\ngeneration entry there that is a symlink, a directory or anything else that is not a regular file\nrefuses the cut, as every other auth record does.\n\nThe continuity class is what the connector declares, capped by what the checkpoint carries. A\nconnector declaring session continuation classifies as `exact`, but reopening a session takes both\nhalves, the pointer that names it and the store that holds its transcript. A checkpoint missing\neither one cannot reopen that session, so it is recorded as `fresh` when the connector declares a\nfresh start and `drain-only` otherwise. A pointer with no store is capped the same way as a cut\ncarrying neither, because it names a session whose bytes the artifact does not contain. A class is a promise the destination is entitled\nto act on, so it never describes bytes the artifact does not contain. The transcript store stays an\noperator input: this repository does not know where a harness keeps its transcript, so `exact`\nrequires `--session-store` to name one.\n\nThe recorded status is read under the same selection rule as the untracked set, so it describes the\nstate the captured bytes can reproduce. The destination re-reads it in the promoted tree and refuses\na difference.\n\nThe untracked selection rule is recorded in the record and is\n`git ls-files --others --exclude-standard -z, excluding .cotal/`. It honors `.gitignore`, so an\nignored file the seat needs does not travel and has to be moved separately. The `.cotal/` exclusion\nis a secrecy boundary rather than a size one: when a seat's `cwd` is also the mesh root, the control\ndirectory is untracked, and without the exclusion the broker trust material, the space account, the\nmanager instance identity's private seed and the seat's own credentials would land inside the\nartifact. A checkpoint carries credential references only; the destination resolves that material\nitself.\n\nA seat whose launch options could not be resolved is refused rather than checkpointed, with the\nmanager's own wording: `imperative launch options have no non-secret durable source (<keys>)`. The\nrefusal arrives at prepare time, so the cut stops before any child does.\n\nA delegated seat (SPEC \xA713.17) is refused at prepare time too, with\n`a delegated seat is not resumed by a later manager; stop it before preserving`. A manager stop\nafter a refused cut retires that seat through its retirement path.\n\n## clean\n\n```bash\ncotal clean <history|store|all> --force\ncotal clean restore-attempt --attempt <id> --force\ncotal clean restore-fallback --attempt <id> --force\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | `history`: target mesh |\n| `--dms` | off | `history`: also clear DM history |\n| `--store-dir <dir>` | `.cotal/nats` | `store`/`all`: JetStream store directory |\n| `--force` | none | Required: destructive, no prompting |\n| `--attempt <id>` | none | `restore-attempt`: exact stale pre-commit attempt; `restore-fallback`: matching healthy committed restore |\n\nOne configurable cleanup verb; every target requires `--force`.\n\n- `history` purges the retained message backlog on the **running** broker (channels, plus DMs\n with `--dms`). The same operation as [`history clear`](#history), which stays as an alias.\n- `store` deletes the **stopped** mesh's JetStream store (`.cotal/nats`): streams, durable\n consumers, and messages. This is the reset for stale on-disk broker state, e.g. durables\n minted by an older, incompatible Cotal generation surviving a `down`/`up` cycle.\n- `all` is `store` plus the space identity (`.cotal/auth`), the local creds and markers tied to\n it, any crash residue a normal `down` would have swept (stale pidfiles, `run/`), and the mesh's\n registry entry; the next `cotal up` mints a fresh identity.\n\n`history` needs the mesh up; `store` and `all` refuse while any recorded mesh process may still be\nrunning or any same-root recorded broker endpoint remains reachable (run `cotal down` first). A\nrecord counts as stopped only when it is absent, empty, or names a pid the kernel reports gone, so\nunattributable content, a removal reservation, or a pid whose liveness cannot be read refuses. They\nalso refuse outright on a root that holds accounts for several spaces: the store and the broker\ntrust record are shared by every space on the broker, so both targets would take out all of them\nand no `--space` can narrow that. `down`, `backup` and `up --restore` refuse there for the same\nreason. `cotal status` lists the tenants on such a root. Personas\n(`.cotal/agents`) and logs are never touched. The mesh record now carries a custom\nstore location for `up`'s own repair, but `clean` still takes `--store-dir` itself; `clean` does\nnot read the record. Custom cleanup targets must contain either the Cotal store-generation marker or a\nreal `jetstream/` store directory; filesystem roots, project roots, and Cotal auth/maintenance trees\nare always refused.\n\n`store` and `all` also refuse every maintenance journal state. After a healthy committed restore,\n`restore-fallback` is the only supported way to remove the recorded unchanged old-store inode; it\nnever deletes the active target, requires both the exact attempt id and `--force`, and retires the\ncompleted restore journal so a later `down --preserve-state` can start a new backup cycle.\n\n## reset\n\n```bash\ncotal reset store <space> [--dry-run] [-y|--yes] [--json|--plain]\ncotal reset space <space> [--dry-run] [-y|--yes] [--json|--plain]\n```\n\nBoth routes select a space this machine started. A space with a locally started registration is\nselected by it. A controller stack that a completed `down` stopped has no registration; its\nprepared record and stack journal at the same root name its broker address, store and mode, and a\nreset keeps both, so a later `up` starts it again. A stack whose last operation is not a completed\n`down` refuses `LIFECYCLE_BLOCKED` and names `cotal down --yes`. Each refusal names one reason and\nthe command that clears it. A space that may still run names `cotal down` from its root, or\n`cotal up` and then `cotal down --yes` when a member exited without a recorded stop.\n\nThe store dry-run is a local-impact preview that exits 0. It requires a space selected that way, a\nreal project `.cotal` and a root readable as one local tenant. It opens with a verdict: the apply is\noffered, with `cotal reset store <space> --yes` as Next, or it would refuse, with the apply's own\nreason and remedy. The JSON result carries it as `applyVerdict`, and `completeness` stays\n`partial`. Completion suggests locally started names, without proving\neligibility. The preview inspects the store path with the existing guarded\ncleanup path checker, reads the maintenance journal, legacy process records,\nservice-controller instances, supervisor receipts, retained native attempts\nand root-local additive ledgers and attempts plus fresh boot attempts, and\nmakes a credless broker probe. Readable deployment IDs, unsettled attempts and unreadable\nrecord paths remain visible; these records do not\nprove live ownership or a complete broker tenant inventory. An orphan receipt\nor attempt remains visible without an instance record; these space-scoped\nrecords are not proven owned by this root. The preview also lists other local\nregistrations at the exact\nrecorded broker address. A match blocks store apply, while no match does not\nexclude aliases, remote tenants or live attachments. A negative credless probe\ncannot prove absence for TLS-first listeners. A custom store path must be\nabsolute. The preview takes no maintenance lock and cannot establish exclusive\ncustody or a complete identity deletion set.\n\nBare `reset store <space>` checks those conditions, displays a scoped terminal\nconfirmation or requires `--yes` in a script, and acquires the maintenance lock.\nUnder the lock it rechecks the exact registration or stopped-stack records, one-tenant root, absent\nmaintenance journal, legacy process records, service instances, supervisor\nreceipts and retained native attempts for that space, additive ledgers and\nattempts plus unsettled fresh boot attempts under the root, credless broker\nnonresponse and pinned filesystem identity before calling the existing\nstore-only cleanup. A readable or unreadable additive ledger or attempt\nblocks reset before confirmation and under the lock. It refuses another\nlocal registration at the exact broker address before confirmation and\nrechecks under the lock. This does not prove\nexclusivity against aliases or remote tenants. An orphan receipt or attempt\nblocks reset even when no instance record remains; a submitted native step may\nhave changed state. A TLS-required or TLS-first target refuses\nbecause a credless probe cannot prove it is down. The route preserves trust,\ncredentials and registration. A cleanup with an unconfirmed outcome is reported\nas unknown and is not retried. Credless nonresponse and the recorded process\nchecks are not proof that no unrecorded process exists.\n\n`reset space <space>` adds local identity cleanup through the existing native\nclean-all operation. This route currently requires Linux, a space selected as above and a\nplain-NATS loopback listener. It removes a locally started registration; a stopped controller stack\nkeeps its prepared record and stack journal. TLS-first, remote, shared-root\nand non-Linux targets refuse. It applies the same maintenance, deployment and\nprocess-custody guards as store reset. It does not stop a process to become\neligible.\n\nIts `--dry-run` validates the existing local trust chain and inventories the\nnative identity paths, including provider-declared secret keys, without running\ndeprovisioning hooks. The inventory is bounded to 10,000 paths. Foreign space\nsegments, symlinks, hard-linked files, foreign ownership, group/world-writable\npaths and providers without bounded cleanup metadata refuse. The refusal names the path and its\nreason; for a group- or world-writable path it names `chmod go-w <path>`. Values and content\nfingerprints are not printed. No maintenance lock is taken by the preview. A preview that passes\nevery check says the apply is offered and names `cotal reset space <space> --yes` as Next.\n\nApply shows the path inventory and defaults to cancellation, or requires\n`--yes` in a script. Under the maintenance lock it repeats the eligibility\nchecks and compares the reviewed file identities. Native secret deletion is\nrestricted to the planned keys; each filesystem removal checks the remaining\nreviewed tree. Added paths and changed files do not enlarge the confirmed scope.\nThe native operation deprovisions secrets before raw identity removal and\nremoves trust last. After checking the planned targets absent, it removes only\nthe unchanged eligible local registration and releases its matching selection.\nA replacement registration is kept and reported as partial.\n\nThe receipt retains acknowledged deletion paths even if later cleanup fails, and lists each path\nonce.\nUnconfirmed effects are unknown and are not retried. Personas, saved service\ndefinitions and stopped-instance records, installed packages, login sessions\nand remote data remain. Local cleanup does not revoke a remote credential or\nclear Cloud data.\n\n## Backups\n\n```bash\ncotal down --preserve-state [--store-dir <dir>]\ncotal backup create <dir> [--only full|registry] [--store-dir <dir>] [--json|--plain]\ncotal backup inspect <dir> [--json|--plain]\ncotal backup verify <dir> [--json|--plain]\ncotal backup recover <full-attempt-id> [--yes|--dry-run] [--json|--plain]\ncotal backup cleanup-fallback <full-attempt-id> [--yes|--dry-run] [--json|--plain]\ncotal backup restore <dir> [--only registry] [--store-dir <dir>] [--accept-missing-source] [--json|--plain]\ncotal up --restore <dir> [--restore-only registry] [--accept-missing-source]\n```\n\nOn a controller root, one with a stack journal, `backup create`, `backup restore` and\n`up --restore` refuse `UNSUPPORTED` (exit 2) in this release. Each needs a preserved cut, and\n`down --preserve-state` cannot make one there. They refuse while validating their arguments,\nbefore the connector seed or any write. Plain `down` and `up` keep a controller stack's store,\nchannels and history. Backup and restore of a controller-owned stack return in a later release. The\nrest of this section applies to a root without a stack journal.\n\nBackup is offline-only. It requires the stable `ready` record from `down --preserve-state`, an exact\nstore match, no recorded process that may still be running (the same test `clean` uses), and an\nunreachable exact endpoint from the recorded cut. That endpoint is probed immediately before cloning, so a live broker with a missing or stale pidfile\nis still refused. It claims the cut, reflink/copies the stopped source to a\nprivate attempt clone, and opens only that clone on a random loopback bootstrap broker with an\nindependent parent/deadline watchdog. A bootstrap broker that does not accept connections within\n15 seconds fails the command. Creation returns one JSON/plain receipt after cleanup.\nA published artifact plus incomplete cleanup is partial and retains the artifact and original\nattempt; it is not a reason to repeat creation blindly. It validates the canonical stream and pull-consumer inventory,\nwrites native snapshots with consumers excluded, and stores conservative contiguous ACK-floor\ncheckpoints separately. The presence bucket is memory-backed, so it does not survive the cut and\nthe clone may lack it. Every other stream must be present. The original store is never opened by\nthe backup broker, and the stack is not restarted implicitly. Artifact destinations must not overlap\nthe preserved source or maintenance\nattempt tree. Restore artifacts and targets likewise cannot nest inside or contain each other, the\npreserved source, or the maintenance attempt tree.\n\n`backup create` checks its preconditions before it claims the cut or creates any maintenance state.\nA root with no preserved cut refuses and names `cotal down --preserve-state`. A cut in another state\nrefuses and names that state and its space. A destination that already exists is a `CONFLICT`\nrefusal, because create writes only a new directory.\n\n`backup inspect` reads the bounded manifest and exact declared file-name inventory. It does not\nhash snapshot or checkpoint contents. `backup verify` additionally reads each named regular file\nand compares its size and SHA-256. Both are read-only: no staging directory, broker, restore or\nsecret upload. Reports omit snapshot bytes, configuration bodies and trust material. JSON retains\nthe declared metadata; terminal file lists are bounded and state their omissions. When a declared\nfile is missing, an undeclared file is present, or a file does not match its declared size and\nSHA-256, the refusal names that file and the fault, and its JSON target carries `file` and `fault`.\n\nFile integrity is not a signature or a restore proof. Matching a manifest does not authenticate\nits author, verify snapshot semantics or prove existing trust will accept a restore. The guarded\nread path refuses unsupported file-protection environments rather than using a weaker reader.\n\n`backup recover <full-attempt-id>` uses the original root's pre-commit restore journal, not a\nmanifest path or selected-space guess. It requires the native deadline and every recorded owner\nto prove the attempt stale, then performs the native rollback. Commit intent forbids rollback.\n`backup cleanup-fallback <full-attempt-id>` requires the matching healthy active restore and\nremoves only its unchanged retained old-store identity; it never removes the restored target.\nBoth review full paths and default to Cancel. Scripts need `--yes`; `--dry-run` reads the plan\nwithout taking a maintenance lock or writing. Apply rechecks the reviewed journal revision under\nthe native lock, preserves unknown outcomes and never retries a deletion. No live-owner override\nor process kill is supplied.\nThe full attempt id has the form `restore-<uuid>`. `cotal status` shows the attempt a root holds\nunder Maintenance, with its state and next step. A refusal for the wrong state names the next step\nfor the recorded one. A completed `cleanup-fallback` retires the journal, so a repeated cleanup\nrefuses with `TARGET_NOT_FOUND` and says the root holds no restore attempt.\n\n`backup restore <dir>` is the canonical restore. It runs the same guarded restore as\n`up --restore <dir>`, with the same lock, claim, trust and space proofs and the same resume, and\nreturns one `backup restore` receipt: the restore attempt, space, root, selection, store, broker\nprocess and whether the manager, delivery and auth service started. The restore follows the\nartifact's own selection, and `--only registry` narrows a full artifact to the registry. Any other\n`--only` value is refused, because the restore could not honor it. The space, broker and runtime\ncome from the preserved cut, so no selector can rebind them. A receipt means the active journal was\nwritten. Its `seatResume` field says whether retained seats were handed to the manager's resume\n(`requested`) or dropped by a registry restore (`not-requested`). It does not confirm that any seat\njoined. Restore needs the stopped preserved cut from `down --preserve-state`. A root with no\nmaintenance journal refuses before the restore takes its maintenance lock, so the folder keeps no\nstate from the attempt, and `up --restore` refuses the same way. A restore that fails after it\nbegan names its attempt in the text and in the JSON target, which also carries `journalState`, and\nits remedy gives the next step for that state. `up --restore`\nremains as the equivalent startup form, and its terminal output is unchanged. Unlike the other\nbackup modes, `backup restore` runs the connector seed first, as `up` does, because it resumes seats.\n\nTwo commands refuse an active restore. `down --preserve-state` begins a new cut only after\nthe restore journal is retired, so it names `backup cleanup-fallback` as the next step. `team boot`\nresumes only a stopped stack whose manager never launched, and a restored stack's manager already\nran, so start it with bare `cotal up` instead.\n\nStopped client-managed KV ordered consumers are ephemeral read residue, not backup state. Backup\nignores only the pinned client's exact stopped shapes: ordinary last-value watchers and the\nwhole-bucket scanner that uses all-history delivery to collapse concurrent tombstones. A bound\nconsumer or any lookalike with a different filter, inbox, lifetime, or other config is still refused.\n\n`full` is the default and indivisible: channel registry, CHAT/DM/TASK/INBOX/DLV, ACL, MEMBERS, and\nvalidated durable checkpoints. `registry` is the sole partial artifact. Presence, derived membership\nfeed, leases, native ephemeral/history consumers, credentials, keys, tokens, owner secrets, and actor\nledger files are excluded. `full` means every transferable message and registry stream, not every\nJetStream resource: endpoint submissions/facts/events/timers/workflow state, contract artifacts, and\nthe records/auth/session stores are nonportable control state. Restore recreates those streams empty\nwith their canonical configs before exposing the normal listener, so active endpoint runs,\nlifecycles, and sessions do not cross a backup. Artifacts are exclusively created `0700`;\nsnapshot/checkpoint files and\nthe manifest are `0600`; `manifest.json` is written last with exact sizes and SHA-256 values. The\ndirectory is trusted operator input: hashes detect corruption, not malicious rewriting.\n\nRestore validates and stages the exact allowlisted artifact bytes before moving or creating a store.\nIt requires the same space and existing trust state. The whole pre-commit window holds a journaled\nliveness claim (coordinator, watchdogs, brokers, absolute deadline): ordinary `up` and a repeated\n`up --restore` refuse while the claim is live, and a stale attempt is recovered only after the\ndeadline has elapsed and every recorded owner is proven dead. A retried `up --restore` handles this\nautomatically; an operator can also recover it explicitly with `cotal backup recover <full-attempt-id> --yes`. Nothing\never rolls back a live attempt. A registry-only artifact restores as registry-only whether or not\n`--restore-only registry` is passed; omitted infrastructure is always created and the exact\npost-restore stream inventory is asserted before commit intent. Ordinary `up` from a preserved cut\nresumes only the exact recorded source store and runtime; a contradicting `--store-dir` or\n`--runtime` fails in preflight.\n\n**Admitting a seat checkpoint.** An ordinary `up` from a preserved cut admits that cut's seat\ncheckpoints before it journals the resume attempt and before any process starts, so a refusal costs\nnothing. Three gates run in order, each naming what it saw.\n\n1. *Integrity.* Every file the record names must be present, a regular non-symlink file, the\n recorded byte size and the recorded sha256, re-stat'd after the read so a file that moved is a\n refusal. Failure here consults no other gate.\n2. *Identity.* The recorded space must match, the recorded `lifecycleUid` must not belong to a live\n incarnation, and the profile revision must match this host's or be resumed under deliberately\n this host's. A differing revision is refused with both digests and the remedy, and there is no\n override: the checkpoint carries the recorded digest and not the config bytes, so nothing could\n run the seat under the recorded revision, and the manager re-digests the same file and refuses\n drift on its own. This gate has no blanket override, which is the only reason the next one may\n have one.\n3. *Recency.* `capturedAt` is compared to this host's clock against the horizon the record carries.\n Inside it, the seat resumes. Outside it, `up` refuses and prints the capture instant, the clock\n reading and the horizon; `--accept-stale-checkpoint` admits it anyway and the exercised consent\n is printed with the actual age. An unreadable `capturedAt` is refused with no override, because a\n freshness gate that fails open is not a gate.\n\nCustody transfers only after all three pass. The destination claims the recorded generation plus one\nby exclusive create, before it launches anything. A lost create means another destination is already\nclaiming that seat, and it refuses with `seat-writer-generation-create-lost` rather than adopting\nthe winner and becoming a second writer. The recorded `lifecycleUid` is reused and never minted, so\nthe resumed seat binds the same lifecycle-keyed durables.\n\nAdmission is reconciled against the inventory the resume is about to hand the manager, and that\nreconciliation finishes before the restore moves a single tree. A checkpoint whose recorded\n`lifecycleUid` is not the one the retained inventory carries describes a different incarnation of\nthat seat, and it refuses with both uids while every live working tree is still untouched and no\ngeneration is claimed. A retained\nseat with no admitted checkpoint refuses the resume by name: an absent checkpoint directory and an\nabsent record are indistinguishable from a seat that was never checkpointed, and a seat that starts\nwithout passing the gates has claimed no generation. `--accept-stale-checkpoint` is recorded in the\nresume journal with the seat, the capture instant, the admitted age and the horizon, so the consent\nsurvives the terminal it was typed into.\n\nThe whole admission is all or nothing. Coverage is settled first, then every gate runs over every\ncheckpoint, and only then is any generation claimed. A refusal at any point leaves every generation\nunclaimed, including a lost exclusive create during the claim itself: the claims that attempt made\nare removed before the refusal is raised, by the exact paths it wrote, so a generation another\ndestination holds is never touched. A claim is a create that can never be made again, so a refusal\nthat left one behind would consume the retry over the same checkpoint set.\n\n**Restoring a seat checkpoint.** Once every gate has passed over every checkpoint, and before a\nsingle generation is claimed, `up` puts each admitted seat's captured bytes back. A refusal here\ncosts nothing for the same reason a gate failure does: no claim has been made and nothing has\nstarted.\n\nA restore never moves or replaces the destination's own control directory. A checkpoint excludes\n`.cotal/` by design, so a seat whose `cwd` holds one, which is the layout an operator gets by\nrunning `up` and `spawn` in a single directory, is refused before anything is staged: promoting a\ntree that cannot contain `.cotal/` over that `cwd` would carry this host's live trust material and\nmaintenance state away with the superseded tree. The refusal names the control directory it found\nand the remedy, which is to give the seat a working tree that is not a workspace root.\n\nEach seat is staged beside its own `cwd`, in `<cwd>.incoming`:\n\n1. every recorded digest is verified again over the files as they are now;\n2. the bundle is cloned into `<cwd>.incoming`, which is refused when that path already exists;\n3. the recorded base commit is verified in the clone and checked out detached, so a bundle that does\n not contain it stops the resume instead of continuing against a different history;\n4. the index diff is applied with `--index` and the worktree diff without it, both `--binary\n --allow-empty`. That order is what puts staged content back in the index rather than only in the\n worktree, and `--allow-empty` is why a seat with a clean tree is still restorable;\n5. the untracked archive is extracted.\n\nEvery seat stages before any seat is promoted. Promotion moves an existing `cwd` aside to\n`<cwd>.superseded.<timestamp>` and renames the staging directory into place, then puts the session\npointer and store files where the destination's connector reads them, then re-reads\n`git status --porcelain` in the promoted tree and compares it to the status the checkpoint recorded.\nA restore that applied without error and produced a different index is a refusal, not a warning. The\ntwo renames are the only steps that touch the path the seat will use, so a failure anywhere leaves\nevery seat's live `cwd` as it was.\n\nThe rename itself claims the superseded name, and a taken name gets a numeric suffix. The timestamp\nhas one-second resolution, so two promotions of the same seat within one second compute the same\npath; a rename onto a name that already holds a tree fails on every platform, and that failure is\nread as taken. Nothing creates the name ahead of the move, because Windows refuses to rename onto an\nexisting directory at all. A superseded tree is the thing that rename exists to keep.\n\n`git` and `tar` run as child processes with argument arrays, never a shell string.\n\nA leftover `<cwd>.incoming` refuses the resume by name. A staging directory from a failed run is the\nonly record of what failed, so nothing removes one automatically: inspect it, remove it by hand, and\nresume. A pre-existing `cwd` is renamed rather than deleted, so a wrong checkpoint costs a rename\ninstead of a tree. When a promotion fails, the renames that attempt made are undone and the staging\ntree is left where it is, as the evidence for what did not verify.\n\nA session pointer whose recorded `sessionId` is not the one the retained inventory reopens is\nrefused before anything is cloned. A session file already present at its destination is judged by\ncontent: bytes equal to the recorded digest are already restored, and different bytes under the path\nthe connector is about to read are refused with both digests rather than clobbered.\n\n`up --restore <dir>` reaches the same admission and the same restore, after the store is restored\nand validated and before commit intent is journaled. A registry-only restore resumes no seat, so it\nadmits and restores nothing.\n\nOne limit is worth stating plainly. The writer generation is claimed by exclusive create inside one\nworkspace root, so it fences two resumes on the same host and does not fence two independent\ndestinations: copy a checkpoint to two roots and both claim the same successor. A real cross-host\nfence needs a coordinate neither root owns.\n\nAuthenticated restores validate the complete\nspace trust bundle before staging, including nkeys, seed matches, JWTs, signers, and space binding;\nfull restores commit to the validated operator, system-account, data-account, and active-signer root\nchain in addition to the static/user authority fingerprint. Because the system account is part of that\ncommitment, a [`cotal up --rotate-sys`](#up) makes every full artifact taken before it unrestorable\nagainst this root: take a fresh full backup after each rotation. The composed commitment is revalidated\nimmediately before store mutation and never includes secret seeds. Restore never creates fresh auth.\nSame-path restores atomically retain the old\nsource at the journaled fallback path; alternate targets retain it in place; a missing canonical\nsource needs explicit `--accept-missing-source`. Quarantine and target restores use current canonical\nconfigs on isolated random-loopback brokers, never expose native snapshot consumers, and publish a\ncommit-intent immediately before the normal listener starts. Archive bytes never instantiate the real\ntarget: after quarantine validation, every stream is re-snapshotted from the validated quarantine\nstate into attempt-owned sanitized files, and the target is restored solely from those. Before that boundary, failure rolls back\nthe attempt-owned target; after it, ambiguity preserves both stores and records forward-repair\nrecourse. The cooperative maintenance lock excludes Cotal commands, not arbitrary raw NATS processes.\n\nBootstrap brokers in every auth mode, including open, mount the store under a local account with\nrandom operation-specific logins only, each carrying the exact per-phase subject permission matrix;\nnormal static credentials and user-auth sentinel/bearer connections are rejected, and no auth\nservice or callout starts. Open mode differs only in its account label, never in authority. Inventory, each stream snapshot,\nrestore initiation, exact upload id, validation, and each checkpoint recreation use separate exact\nauthorities. Every checkpoint carries the source stream's message/first/last sequence state and must\nmatch its snapshot record before mutation; core then derives and validates the only allowed start\npolicy. TASK is not a CLI exception: the same core checkpoint API recreates its canonical `DeliverAll`\nWorkQueue durable because acknowledged tasks are absent from retention and NATS forbids a\nstart-sequence policy there. Registry-only restore creates every omitted canonical stream and transient\nbucket on the isolated target before the normal listener is exposed. It deliberately does not resume\nretained agents or recreate their DM/DLV/TASK/ACL state; their identity material stays retained and\nstopped rather than being reprovisioned into a partial restore.\n\nAfter listener readiness, the manager starts attempt-bound, validates retained credentials/tokens\nwithout granting or reprovisioning, and resumes the exact persisted principals under cleanup\nsuppression. Registry-only restore uses the same flow with an empty agent set. On a user-auth mesh\nthese manager calls run as the logged-in operator's `cli` actor, the caller the preserve cut used, so\nthat actor needs a current `admin` grant. `commitResume` is an\nidempotent validation barrier only: success must be `awaitingFinalize` with an attempt-bound 64-hex\ncommit token and does not release suppression. Under the workspace lock, the CLI first fsyncs that\nexact evidence as `manager-committed` (restore) or `resume-committed` (ordinary resume), then calls\ntoken-bound `finalizeResume`; only an `active` response for the exact token releases suppression. The\nCLI records the same token in finalization evidence before a restore becomes `active`, or before an\nordinary resume retires and consumes the marker. A new launch hands the token to the next manager as\na protected file: the CLI journals its path, writes it at mode 0600 in\n`.cotal/maintenance/v1/handoff/`, then journals its inode and digest before the child starts. The\nchild reads it once with `--resume-commit-file` and `--resume-commit-sha256` and removes it. The\ntoken never appears on a new launch's command line. Before retirement or activation the CLI settles\nthat file. A file it did not record, or a recorded path that now holds another file, holds the\nattempt and is never deleted. `cotal supervise --resume-commit-token` stays accepted for older\ncallers; while such a process runs, its token is visible in its command line. Re-entry from either committed state skips the prior\nidempotent activation/commit phases, retries finalization with the durable token, and finishes the\nworkspace transition. Failure before finalization preserves the committed state and cleanup\nsuppression; it is not rewritten through a degraded transition. Re-entry between any two earlier\nboundaries reuses the same attempt and may retry the idempotent phases without deleting retained state. A missing or\nchanged per-agent dependency is a named fail-closed result; the journal becomes degraded and remains\navailable for forward repair. A retry from `resume-intent`,\n`resume-active`, or `resume-degraded` reuses the same attempt and inventory after the prior listener is\nproven stopped. A retained agent the lost manager already launched can still be running, for example\nin a tmux window, while the journal reads `resume-intent`. On a static mesh the replacement manager\ncloses that seat through the reference the lost manager recorded on the agent's slot, waits for the\nprincipal to leave presence, and launches it again. A live principal with no such record, or one that\nstays live after the seat is closed, is refused. Every normal restore listener has an unguessable\nattempt-bound NATS server name. The CLI fsyncs its exact name/nonce, canonical endpoint, process owner, and generation-bound target identity\nimmediately after spawn. Re-entry accepts a surviving listener only when its INFO server name, live PID\nrecord, endpoint, and target identity all match that proof; degraded restore repair then moves through\nthe guarded workspace transition only after manager commit. If an uncommitted bound owner is provably\ndead, recovery retires that exact proof under the maintenance lock and binds a fresh listener for the\nsame attempt, endpoint, and target with a new nonce and server name. A live foreign/mismatched listener\nor ambiguous owner is preserved and refused, never adopted by reachability alone. A reconstructed\ncommit/degraded attempt without either the exact bound proof or a durable dead-listener replacement\nrecord fails closed even when the recorded port is free. A later ordinary startup may pass an `active`\nrestore only when its details prove manager commit and its exact recorded listener is dead.\n\n## Selecting a space\n\n```bash\ncotal space [--json|--plain]\ncotal space show [<name>] [--json|--plain]\ncotal space add <name> --server <url> [--root <dir>] [--mode auth|open] [--tls] [--allow-unencrypted-overlay] [--force] [-y|--yes]\ncotal space add [<name>] (-f|--file <bundle.json> | --from <https url>) [--root <dir>] [--server <url>] [--force] [-y|--yes]\ncotal space create <name> [--root <dir>]\ncotal space forget <name>\ncotal space delete <name> [--dry-run] [-y|--yes] [--json|--plain]\ncotal use [<space>] [--json|--plain]\n```\n\n`space` lists the spaces this machine can reach: each one's broker state, server, mode, origin\n(`started here`, `registered` or `discovered`) and root, with the selected one marked. It probes\neach broker that this machine started or that was registered by hand, and reports one that does not\nanswer as `unreachable`; its registration stays. A discovered space is never dialed by a listing and\nreads `not checked`. `space show [<name>]` shows one space in full, the selected one when no name is\ngiven, and probes only that broker. Neither writes anything.\n\n`--json` writes one document. `space` gives `current` (the selected space or null),\n`currentRecorded`, `items` and `prepared`; each item is the row `cotal status --json` gives under\n`meshes.items`: `space`, `server`, `mode`, `root`, `origin`, `tlsRequired`, `eventsRequired`,\n`current`, `check` (`reachable`, `unreachable` or `not-checked`) and `catalog`. `space show` adds\n`recordedAt`, `attachHost`, `maxSessions`, `storeDir`, `unencryptedOverlay`, the login pins of a\nuser-auth space under `userAuth` and the catalog's `id`, `slug`, `kind` and `role`. It leaves\ncredential paths out. A name that is not registered is refused with `TARGET_NOT_FOUND`, exit 2, and\nso is a bare `space show` with nothing selected. Each entry under `prepared` has `space`, `root`,\n`preparedAt` and `current`, and `space show` of a prepared space gives those with the kind\n`prepared-space`.\n\n`space add` registers a space that runs elsewhere, so `--space` and `use` can reach it. With\n`--server`, the trust comes from `--root`'s `.cotal/auth`, and the mode is `auth` when that folder\nholds the space's trust, `open` otherwise. With `--file`, a user-auth connection bundle exported\nwhere the space runs supplies the login pins and the server, unless `--server` names another\naddress for that broker; `--from` fetches the space's discovery\ndocument instead, after you agree to the fetch, and needs a terminal. Before anything is written,\n`space add` checks that the address may carry credentials, that the trust composes, that the\nbroker enforces the mode, and that the credentials open the space; a user-auth space's exchange\nmust answer as the issuer it pins. On a terminal it then shows what it will trust and asks; `--yes`\nskips that question, but not the confirmation of fetched pins. It never changes the selection.\n\nA refusal writes nothing and carries its code: `USAGE_INVALID` or `SCHEMA_INVALID` (exit 1) for\ninput, and `CONFLICT`, `TARGET_UNAVAILABLE`, `DEPENDENCY_UNAVAILABLE`, `UNSUPPORTED`,\n`AUTH_REQUIRED` or `PERMISSION_DENIED` (exit 2) for what the checks found. A name that is already\nregistered is a `CONFLICT`. `--force` replaces a registration added by hand, never one this machine\nstarted or a catalog lists, and adds a `--server` space without checking its broker, which suits a\nspace that is down right now; a user-auth space is always checked. Declining the question is a\n`no-op` with a `USER_CANCELLED` warning. `--json` gives the registration under `space`, in the\nshape `space show` gives, with `verified` and `replaced`.\n\n`space create <name>` prepares a space to start on this machine. It records the name and the root,\nwhich is `--root` or this folder, and makes `<root>/.cotal/`; `cotal up` in that root then starts the\nspace under that name and drops the prepared record. It also writes the `default` persona into\n`<root>/.cotal/agents` when the root has none, as setup does, and leaves one you edited as it is.\n`--json` lists the files it wrote under `result.written`. The project and its ancestors must be physical\ndirectories owned by you or root, with no symlinks. World write refuses. Group write is allowed only\non a directory you own whose group is your user private group, the Debian and Ubuntu default, and\nthat has no extended ACL. On Linux, `getent` must show that the group is your primary group, lists\nno other member and is no other account's primary group, and `getfacl` must show no ACL entries\nbeyond the base ones. That account list counts only when `/etc/nsswitch.conf` names `files` and\n`systemd` alone for passwd and group. A directory source such as LDAP or SSSD can list part of its\naccounts, so with one configured, group write refuses. `getent` and `getfacl` run from your PATH\nwith a 2 second limit each, so the first check in a process can take a few seconds. A proven group\nholds for the life of the process. When a read is unavailable, group write refuses; install the\n`acl` package for `getfacl`, or remove group write. Root-owned sticky temporary directories are\nallowed. New control directories use mode 0700. An unsafe existing path refuses before the\nprepared record is written, without changing its permissions. The refusal names the path, the\nreason and a remedy such as `chmod g-w <path>`. It starts nothing and leaves the\nselection as it is. It refuses with `CONFLICT` a name that is registered or prepared, a folder that is already a\nspace's root, and the home folder, whose `.cotal` holds this machine's own records. `space` lists\nprepared spaces under their own heading, and `space show` and `use` accept one. A command that\ntargets a prepared space refuses with `TARGET_UNAVAILABLE`, exit 2, naming the root to start it in,\nand so does `cotal up --space <name>` run from another folder. `cotal up --space <name>` in a folder\nthat is already another space's root refuses with `CONFLICT`, exit 2. `space add` refuses a prepared\nname.\n\n`space forget <name>` removes a registration this machine keeps. It stops nothing, deletes nothing on\nthe broker, and keeps the files under its root. It refuses a space this machine is running with\n`LIFECYCLE_BLOCKED`, because `cotal down` stops it and drops the record, and a discovered space with\n`CONFLICT`, because its catalog keeps it. A space whose root holds its stack journal counts as\nrunning while any stack member's process may still run. The refusal names one such member and\nwrites nothing: the registration, the selection and the root's files stay. A member whose process\nis gone without its stop receipt does not block, and the result warns with `STACK_MEMBER_UNSETTLED`\nthat it was never stopped cleanly. A prepared space is forgotten the same way, and the `.cotal/`\nthat `space create` made stays. Forgetting the selected space clears the selection.\n`status` in a forgotten space's root then selects no space and probes no broker.\n\n`space delete <name> --dry-run` reads the registered broker in open or static-auth mode.\nIt requires the broker's mode to match the registration, and a static connection must\nconfirm its current account and user identity. It reports the known first-party\nstreams and bucket backing streams as present or absent, the broker-observed\ntransfer-stream targets under a second exact-name grant in static mode, and the\nentries in the presence bucket. Both static connections must confirm their own\nidentity and the same broker server ID. A failed or incomplete native read refuses\ninstead of showing missing resources as absent. The snapshot is non-atomic and presence is not an\nauthoritative liveness or membership check. User-auth deletion inventory and apply\nare not supported. The dry-run removes no live or local state, and a read that covers its whole scope\nexits 0. A space that is prepared and not running, including one stopped with `cotal down`, has no\nlive broker to read or delete from: `space delete` refuses it with `TARGET_UNAVAILABLE`, exit 2,\nnaming the root to start it in, and `space forget` removes only its local record.\n\nOn a space this machine started, the apply refuses `LIFECYCLE_BLOCKED` while the space's own stack\nstill runs here, before any broker connection. On a controller root that is any service instance of\nthe space other than its broker, and on a legacy root any recorded daemon other than the broker.\nDeleting the streams under them leaves a stack that can neither start nor stop. The refusal names\nthe running services and `cotal down`, then `cotal reset store <space> --yes` on a controller root or\n`cotal clean store --force` on a legacy one to remove the stored streams. The dry-run shows the same\nrefusal as a warning.\n\nBare `space delete <name>` displays that impact on an interactive terminal and requires\ntyping the exact space name. A script supplies the exact positional name with `--yes`;\n`--json` without `--yes` refuses before a connection. After confirmation the command\nreads the broker again and refuses if the stream states, transfer names, presence\nidentities, registration, trust or broker identity changed. It opens a separately\nminted exact-target connection, checks each stream state again before the first delete,\nand submits one deletion at a time. A native acknowledgement is reported as deleted;\nprior absence is separate, and the receipt lists only the groups that have a name. It then checks each target's stream info and enumerates\ntransfer names again. A surviving or newly observed resource produces a partial\noutcome if deletions were acknowledged; an incomplete post-read stays unknown.\nA refused or unconfirmed delete stops the sequence without retry, reporting\nacknowledged names and refused, unknown or remaining targets. This is not atomic:\na live process can recreate a stream after the final read. The result\ndoes not claim that processes stopped, or remove registrations, credentials or local\nfiles. Open-mode impact and receipt label that any broker client can delete the space.\n`space forget` is not a substitute for live deletion.\n\n`use <space>` selects the space commands target when none is named. It changes only the selection:\nit starts nothing and does not check the broker, so a stopped or prepared space can be selected and\nthe command that targets it reports that. Selecting the selected space again is a `no-op`. A name that is not\nregistered is refused with `TARGET_NOT_FOUND`, exit 2, and the selection stays. Bare `use` reports\nthe selection. A selection that names a space no longer registered is reported with a\n`SELECTION_NOT_REGISTERED` warning by `use` and `space`, and stays until `use` changes it.\n\nA command run without `--space` targets the selection. When the selected space's broker is down it\nrefuses with `TARGET_UNAVAILABLE`, exit 2, and when the selection names a space no longer registered\nit refuses with `TARGET_NOT_FOUND`. Neither reaches another mesh.\n\nWith nothing selected, a command run in a project targets the space registered for that project's\nroot. A root with no registration whose `.cotal/` holds the stack journal `cotal up` writes is\nrefused with `TARGET_UNAVAILABLE`, exit 2, naming the broker the journal recorded and the root to run\n`cotal up` in. That is the state after an `up` that failed to start or a `space forget`, and the\ncommand never dials the default address instead. `status` there reads the machine, selects no space\nand probes no broker.\n\nWith no `--space`, `--server` or `--creds`, nothing selected and no project in the folder, a command\nthat several running spaces could serve asks which one when it runs in a terminal. The answer is the\ntarget of that one run, and the selection stays as it is; cancelling the question does nothing and\nexits 0. A script, a `--json` caller and `--no-input` are not asked: the command refuses with\n`TARGET_AMBIGUOUS`, exit 2, naming the running spaces. Every refusal of a target puts what to run next\nin `error.remedy`, apart from `error.message`.\n\nA manifest's `space:` is the target of native `up -f` and the new team\ncheck/boot/deploy routes. A `--space` that names another space is refused before\nanything is written. The old `spawn -f` manifest spelling no longer deploys;\n`team deploy` applies all-new live channel-only manifests or bounded\nstatic/open agent and mixed manifests. Reapply remains unsupported.\n\n`cotal meshes` still lists, registers and drops meshes. `space`, `space add`, `space create` and\n`space forget` replace it, and its `--json` lines are the old shape. A bare `meshes add` on a terminal is still the\nguided form.\n\n## Mesh registry\n\n```bash\ncotal meshes [--json]\ncotal meshes add # guided, on a terminal\ncotal meshes add <space> --server <url> [--root <dir>] [--mode auth|open|user] [--tls] [--allow-unencrypted-overlay] [--force]\ncotal meshes add <space> --mode user (--user-auth-file <bundle.json> | --from <https url>)\ncotal meshes rm <space> [<space> \u2026] [--force]\ncotal sync [--idp <auth base URL>] [--json|--plain]\ncotal status [--json|--plain] [--all] [-v|--verbose] [--space <s>] [--server <url>]\ncotal status --components [--space <s>] [--server <url>]\n```\n\nEveryday command forms use positional agent names: `cotal attach <name>` and `cotal stop <name>`.\nFor one-shot messages, choose one destination with `--to/-t`, `--channel/-c` or `--role/-r`:\n`cotal send -t reviewer \"Please inspect the patch\"`. Message receipts do not repeat the body.\nThey distinguish storage from delivery and say that no reply listener remains active.\n\nBare `cotal` and `cotal help` show a compact list of useful actions. Use `cotal help --all` for\nthe full command catalogue. `cotal cfg` is the shorthand for `cotal config`. Its default view\nshows effective values and sources; `config --verbose` adds the storage path and revision.\n\nStatus starts with This machine, groups registrations under Brokers, then shows the selected\nspace, this folder's maintenance journal under Maintenance when it holds one (its state, attempt\nand next step; `maintenance` in JSON), unsettled fresh and additive attempts in this folder when present, owned\nservice records and useful commands under Next. Connection state, local process state and service replies\nremain separate observations. Other registrations say Not checked unless `--all` reads them.\nUse `status --verbose` for installation paths, extension records and the detailed local diagnostics.\nThe default report does not include the agent roster; use `ps` or `who`.\nThe deployment-attempt section lists at most 100 fresh and 100 additive\nlocal IDs, their returned/unsettled state and unreadable paths, with full\nreadable coordinates in JSON.\nIt appears only when there is something to inspect. A missing fresh summary\ndoes not prove the original broker never started; an additive intent does not\nprove which resources changed. This read grants neither retry nor stop\nauthority. If the folder inherits\nthe machine's home `.cotal`, status warns that unqualified `up` refuses instead of starting\nthere. Prepare the intended folder first with\n`space create <space> --root .`; `up --root .` then names it.\n\nOwned services use the saved controller definitions and instance records for the selected space.\nFull instance IDs and execution hosts stay visible. Process observations and historical readiness\nare separate columns; neither a saved serving state nor a supervisor receipt becomes a fresh\nendpoint response or OS unit-state check. Legacy PID-file rows are labeled separately. On a root\nwhose controller stack serves the selected space, its members are the `stack-` rows here, the\nlegacy rows and the stored-state hint are omitted, and when the stack does not answer, Next offers\n`cotal up --root <root>`. Unreadable\nservice record families make the report partial instead of becoming empty lists. Warnings retain\na classified cause code, such as unsupported host observation, without native diagnostic bodies. These reads\nimport no provider and start, stop, adopt or repair nothing. `--all` extends broker checks; this\nlocal owned-service section retains its explicit selected-space scope.\n\n`meshes` lists the meshes this machine knows; a `*` marks the `current` default a bare\n`cotal spawn` joins. Entries learned from a signed-in account are marked `discovered`. Their\nregistration trust is stored under the account's private auth state, and the registry contains no\nsession token or sentinel credential bytes. Commands resolve the catalog `slug`; a different human\n`name` is rendered only as a label.\n\n`meshes --json` prints one JSON object per recorded mesh per line: `space`, `server`, `mode`,\n`root`, `default` (the `*`), and `origin` (`up`, `manual`, or `catalog` for a discovered entry). A\nlocal or hand-registered entry also carries `offline`. A discovered entry is never probed, so it has\nno `offline` field. `tlsRequired`, `events: \"required\"` and a discovered entry's `catalogName` appear\nonly when the record has them. An empty registry prints nothing and exits 0. The note about a default\nthat matches no record goes to stderr, so stdout carries only rows, on a first run too. The table is\npresentation and is not a stable parsing target. `meshes add` and `meshes rm` refuse `--json`.\n\nA registry record this build cannot use is refused by name, never rendered and never skipped. One\nthat does not parse, or is missing a field every consumer reads (`server`, `mode`, `root`, `ts`,\n`space`), makes every registry command exit 1 with the file's path and what is wrong with it.\nRemove the file or restore the record; nothing repairs or invents a field for you. A registry\ndirectory that exists but cannot be read, such as one without read permission or a regular file in\nits place, also makes every command that reads the registry exit 1 with its path and the read\nerror. Only an absent directory is an empty registry. A record removed between listing and reading\nis omitted from that listing. A keyed read also treats a record removed during its read as absent;\nparse, shape and other read failures still refuse by name. Recording or removing a mesh also sweeps\nthe directory for an older-format record of the same space. Any error listing the directory for\nthat sweep, other than an absent directory, fails the operation with that error.\nAn unknown `--space` names the missing registration and points to `cotal space`, which also\nlists prepared spaces.\n\nAn IdP may advertise a same-origin space catalog during login. Cotal reads the complete snapshot and\nadds every valid registration without a separate `meshes add`. A snapshot younger than five seconds\nis used without a request. After that, commands that resolve a mesh target conditionally refresh the\nsaved catalogs. An operation targeting a discovered space refreshes only that space's account and\nrefuses if that account fails. Operations targeting local or manually registered meshes refresh every\naccount, print one warning for each failure, and continue. `cotal status` refreshes every account,\nnever refuses on a refresh failure, and lists each account as `fresh`, `updated`, `not-modified`,\n`no-catalog`, or `failed` with its error. With no cached sign-in, catalog preparation reads without\ncreating a control directory or lock. An explicit IdP without a cached login refuses with\n`AUTH_REQUIRED`. An unreadable sign-in or catalog cache refuses with `DEPENDENCY_UNAVAILABLE`\nbefore refresh or registry application. Both exit 2 and leave the cache untouched; diagnostics\nname the path without printing cache contents or parser excerpts.\n`cotal sync` bypasses freshness and reports added, changed, removed, unchanged, and name collisions. `--idp` limits it to one signed-in account. It never connects\nto a broker. `--json` gives each account under `accounts` with its `idp`, `state`, `catalog`, the\nfive name lists, `selectionCleared` and `error`. An account whose catalog could not be refreshed keeps\nits last snapshot and is named in a `CATALOG_UNAVAILABLE` warning, so the result is partial, exit 4;\nwhen no account could be refreshed, `sync` refuses with `DEPENDENCY_UNAVAILABLE`, exit 2.\n\nThe registry is updated under the same lock that guards the catalog cache, so a command never lists\na discovered space set that another command is still writing. The cache records a fetched snapshot\nas not yet applied before the first registry write and as applied after the last. If a command dies\nor is stopped in between, the next command applies that snapshot again before it can use it, with\nno request inside the freshness window.\n\nThe shared dispatcher applies this preparation to every command that declares both `--space` and\n`--server` as mesh-target flags, including commands registered by other packages and commands that\ndeclare their own equivalent flag objects. Daemon and startup commands that use those names only as\nconfiguration explicitly opt out. Registry-local `meshes add` and `meshes rm` never refresh a catalog.\nWhile the registry holds a record this build cannot use, the preparation neither refreshes nor\napplies a catalog, so the command's own checks run first. A snapshot left unapplied is applied by the\nnext preparation after the record is restored or removed. A command that resolves its target through\nthe registry still refuses the record by name.\n\nRun on a terminal with the space or `--server` missing, **`meshes add` is guided**: it asks for the\none thing that cannot be derived (the broker URL), probes it, and tells you what answered - open or\nrequiring credentials. It then offers the spaces your `--root` already holds credentials for, states\nthe mode as a fact about that broker rather than asking, and shows the exact record before writing\nanything. A broker that does not answer, or a space name already registered, becomes a choice rather\nthan an error. Anything you pass on the command line is taken as given and not asked again. Without\na terminal - a script, an agent, CI - nothing prompts and the flag form's errors stand\n(`COTAL_NO_PROMPT=1` forces that too).\n\n`cotal up` and `cotal down` maintain their own records. `meshes add` registers a mesh they cannot\nspeak for: one running on another machine, a shared broker, a hosted space. `--root` is the folder\nwhose `.cotal/auth` holds that mesh's credentials and whose `.cotal/agents` holds its personas.\nThe default is the project you run it in. The registry stores that path, never a secret. `--mode`\ndefaults to `auth` when the root holds the space's account record and to `open` otherwise. The\nbroker is probed before anything is recorded, so a wrong address, or credentials that mesh will\nnot accept, fails here instead of at the first `spawn`; `--force` records without verifying (and\nreplaces an existing record).\n\nA hostname or public address is registrable only when the connection will **require TLS**. Pass\n`--tls`, or use a `tls://` URL. The scheme is recorded as enforced intent, so every later dial\nthrough the record demands the handshake (and `meshes add tls://\u2026` against a plaintext broker is\nrefused at registration). Without required TLS the fence admits loopback literals, and\nprivate-overlay literals only with `--allow-unencrypted-overlay`, which records your acceptance that\nthe overlay is protected only while its tunnel is up ([Run a mesh](run-a-mesh.md#which-addresses-you-may-register)). RFC1918 addresses are refused in both modes because a cafe LAN is private but does not belong to you.\n\nA **user-auth** mesh registers from supplied pinned trust, never guessed: `--user-auth-file`\ntakes the bundle exported where the mesh runs; `--from` asks before it dials the address at all,\nthen fetches the `/.well-known/cotal-mesh` discovery document under that address (HTTPS only; a URL\nthat already ends in that path is fetched as given), displays the pins, and asks again before\nadopting them. The document must parse as JSON under any content type the host serves it with; an\nanswer that does not is refused with the URL and the content type that came back. Neither fetch follows redirects: a 302 can move a pinned fetch\nonto plaintext or onto another host, so it is refused rather than followed, and the pinned\nexchange must itself be an `https://` URL, except for an exchange on this machine, where plain\n`http://` is accepted for a loopback *literal* (`127.0.0.1`, `::1`, any spelling of them) but not\nfor `localhost`, which is a name rather than an address. Registration verifies that the exchange\nanswers `/health` and `/jwks` as the pinned issuer. It also verifies that the broker refuses a bare\nconnect; that auth-required refusal is the pass. The sentinel credentials land in a 0600 file under\nthe entry's root; the registry records only the path.\n\n`meshes rm` drops records. It never stops a mesh. For a mesh running on this machine `cotal down`\nis the right verb, and `rm` says so unless you pass `--force`. A broker record that cannot be proven\nstopped counts as running, as it does for `clean`. A hand-added record is removed by\n`meshes rm`, by an `add --force` replacement, or by a `cotal up` that actually starts the broker for that same space, server and root, which becomes that\nmesh and so takes the record over (a `cotal up` for that space anywhere else refuses instead).\nNothing that merely *infers* a record is stale from a dead broker touches it: an\nunreachable broker is listed `offline` and stays, whether `cotal up` or `cotal meshes add`\nwrote the record; a foreground `up` whose broker exits unexpectedly keeps its record the same way.\nA bare command does not treat that offline record as a running mesh;\nname it with `--space` to restart it. When a bare control command resolves its target, it probes the\nrecorded brokers once, and its mode check and its connect both read that one result. `cotal down` / `cotal clean all` still drop an `up` record for the project\nthey tear down; a hand-added one they leave alone even when it shares a root, because nothing\non this machine could write it back.\n\nA discovered entry belongs to the normalized IdP origin and proved subject that supplied it. Local\nteardown, cleanup, and liveness pruning do not remove it. A manual or locally started entry with the\nsame name wins and remains untouched; that discovered name is reported as a collision. Logging out\nremoves only the discovered entries owned by that account.\n\n`cotal meshes` and `cotal status` print `events: required` for a registration carrying\n`policy: { events: \"required\" }`. On that space, foreground spawn, detached spawn and manager\nstarts that host a native harness cannot opt out or launch without an event plane. `--no-events` is refused with\nthe space named. A connector without an event plane is refused with both the space and connector\nnamed. A session whose own grant omits `events.<owner>.<actor>` is refused before joining and the\nmessage names a full-row `actor grant` repair. A running seat whose event plane stops keeps running\nand keeps its mesh connection on that space as on any other; the connector re-establishes the plane\nitself and reports every failure with the delay before the next attempt. A seat the manager resumes\nfrom a preserved cut meets the same rules as a fresh\nspawn: a seat retained with `--no-events`, or on a connector without an event plane, is refused, and\na resumed seat carries the requirement in its launch material.\n\n`join`, one-shot sends and passive protocol views host no native harness turns. They publish no\nAG-UI runs, regardless of who drives their input. Required events do not turn terminal messages\ninto agent runs or copy channel and DM text into an event stream. This launch policy is not an\nadditional broker authorization boundary. The broker still enforces each endpoint's grants.\n\nThe selection `cotal use` sets applies from every directory, including inside another mesh's\nproject. `status` is a read-only report in this order: this machine\n(the installed `cotal-ai` version, NATS and each named integration), the installed extensions and\ntheir versions, this folder's `.cotal/`, the recorded meshes, the selected mesh (its connection, the\ndelivery responder, its channels and membership feed), then the hints. It lists no roster; `cotal ps`\nlists managed seats. Stale Claude skills and out-of-date `.agents` skills recommend\n`cotal setup --skills`, not unscoped `cotal setup`. `status` takes `--space` / `--server` to pick the\nmesh to inspect; it starts nothing.\n\nStatus displays broker addresses without URL user information, query parameters or fragments,\nincluding component facts. Saved addresses and connection inputs are unchanged. An address that\ncannot be displayed safely is `null` in JSON, `unavailable` in text, and names a\n`BROKER_ADDRESS_INVALID` warning with a partial result. Its broker component is refused.\n\nOnly the selected mesh is checked. Its row in the recorded meshes dials its address with its\nrecorded TLS requirement, and every other row reads `not checked`. `--all` checks every recorded\nmesh, including discovered ones, each with its own recorded TLS requirement.\n\n`status --json` writes one envelope whose `target` is `{space, scope}`, where `space` is the selected\nspace or `null` and `scope` is `selected` or `all`. Its `result` is the report the text draws:\n\n| Field | Holds |\n|---|---|\n| `kind` | `infrastructure` |\n| `machine` | `version`, `install` (`kind` and `root`), `nats` (`path`, `bundled` or `missing`), `integrations` (each `{name, state, text}` a connector's setup reports), `connectors` (each `{name, requires, missing}`), `skills` (`state`: `current`, `not-installed`, `behind` or `unreadable`), and `web` (`installed`, `url` and the error of each when it could not be read) |\n| `extensions` | `root` and `items`, each `{package, label, version}` |\n| `folder` | `root`, `auth` (`state`: `space`, `spaces`, `none` or `unreadable`), `personas` (`root`, `default`: `valid`, `invalid`, `missing` or `unreadable`, `demo` and `error`), `launches` (whether a bare spawn launches from this folder's catalog; `null` when no mesh resolves), `processes` (each `{name, state, pid, detail}`, `state` one of `running`, `stopped`, `not-serving`, `unknown` or `unreadable`) and `storedState` |\n| `meshes` | `current`, `unreadable` (the registry directory, a record or the current-mesh pointer that could not be read, or a pointer that is empty, each as `{file, error}`), `items` (each with `space`, `server`, `mode`, `root`, `origin`, `tlsRequired`, `eventsRequired`, `current` and `check`: `reachable`, `unreachable` or `not-checked`) and `catalogs` (each `{idp, state, error}`) |\n| `selected` | `{resolved: false, code, message}` when no mesh resolves (`code` is `registry-unreadable` when the registry directory or a record could not be read); otherwise the target's fields, `connection` (`ok` or the reason it failed; on a user-auth mesh `reachable` or `unreachable`), `login`, `deliveryResponder` (`bound`, `unbound`, `stale` or `unknown`), `snapshot`, `channels` and `membershipFeed` |\n| `services` | Selected-space local owned records: `space`, `state` (`read`, `partial` or `not-checked`), non-atomic `observedAt`, public definition/instance projections and explicit scope. An unreadable family is `null`; stored readiness is historical. Older serialized snapshots may omit this observation, which is not an empty inventory. |\n| `hints` | The hints, in the order the report prints them |\n\nAn unreachable broker, a stopped process or an unbound responder is a state the report names, and\n`status` exits 0. A part it cannot read is named on its row and as a warning, and the run is\n`partial` with exit 4: a PID record, the folder's trust material, the delivery-aware marker, the\nconnectors, the skills bundle, the web records, a persona catalog, the mesh\nregistry or one of its records, the current-mesh pointer, a user-auth login, or the selected mesh's\nchannels or its owned service definition/instance records. Only an absent catalog, registry or pointer reads as empty or unset; an empty pointer is\nnamed too. A registry directory or record that cannot be read also leaves the selected mesh\nunresolved, because resolution refuses it, and the readable records are still listed. A corrupt or\nunreadable extensions manifest is refused while loading the command surface, before this report\ncan run. The manager\nrow asks the service endpoint once: a live process that does\nnot answer is `not serving`, and a probe that could not be made leaves the row\n`running \xB7 service unchecked`.\n\nThe local auth reader uses a stripped signer only when both split trust records are absent.\nThe load refuses dangling and looping links at either record even when an older stripped signer is present.\nA process row whose PID record exists but cannot be read, or exists under both its current and a\npre-upgrade name, reads `pidfile unreadable` with the error, and the other rows still print. A live\nmanager whose delivery-aware marker cannot be read keeps its row and names the failure as\n`delivery-aware marker unreadable` with the error. The `Web process` row\nprints the address the selected mesh's dashboard recorded once it was listening, while the PID in\nits `web.pid` is alive. The dashboard that holds `web.pid` picks the record. A dashboard started with\n`cotal service start` writes its session record beside its service startup receipt, and status finds\nthat record through the web service instance whose recorded process is that PID. Any other holder is\nread from `web.session`. Neither record is read in place of the other. A `web.pid`, session record or\nservice instance record that exists but cannot be read is named on the row with the error.\nOtherwise it reads `down`, or `not installed` without the web\nextension. Status and the `cotal setup` card count the web extension as installed only when a\npackage in the extensions manifest provides `web` and that package is on disk. A web package record\nthat cannot be read, is not JSON or has no `version` is named on the `Web extension` row and on the\ncard's web row, after the dashboard's address when it is listening and beside an unreadable\n`web.pid` or session record. The `Web process` row still prints, and reads `down` unless the\ndashboard is listening.\n\nIf a refresh fails, `status` may still show the kept catalog bytes for diagnosis. It labels them\nstale with the last successful snapshot timestamp and the refresh error. It never calls that state\nsynchronized or online. If a selected discovered space vanishes from a successful snapshot, the\nselection is cleared and the command reports that no default is selected.\n\nFor a user-auth mesh the selected-mesh section reports the login `status` works as: the signed-in\nsubject when this machine holds a cached session for the entry's pinned IdP, or the exact `cotal\nlogin --idp <url>` line when it does not, with no network round trip either way. A locally\nprovisioned space also shows the actor grant row; a discovered or registered remote entry reports\nthe grant as not checkable on this machine, because the ledger runs where the space was\nprovisioned. `--components` on a user-mode target probes as that same signed-in login (`ps`'s\ncredential), never a static mint; when the login cannot supply a credential, the row says why\ninstead of printing the broker's refusal of an unauthenticated probe.\n\nPersona rows name the catalog they describe. If this folder and the selected mesh use different\ncatalogs, status names both and marks which one spawn launches from. A green `default` means the file\npasses the same agent-file loader spawn uses; a present but invalid file is reported as invalid.\n\n`cotal status --components` prints the report, then a fail-loud per-component health pass. It takes\nno `--json`, `--plain` or `--all`. It reads **each\ncomponent's own control surface**, rather than treating a PID, a lease, or a successful probe of a\nsibling as proof that the component serves. It prints one of `serving`, `absent`, `not-serving`, or\n`refused` for each component and exits `0`, `1`, `2`, or `3` respectively (the highest observed\nstate wins). That exit replaces the report's own, so a partial report followed by a clean pass exits\n0:\n\n- **manager**: local PID record, its liveness-lease holder and PID, then the manager's own typed\n `status` service reachability from this host. Manager builds that do not report static\n reconciliation say `static reconciliation not reported by this manager build`; the line stays\n visible even when the manager is otherwise `serving`.\n- **delivery**: local PID record, its ready lease (`ready` is the daemon's own bound-control\n signal), and the latest `renewal.<spaceKey>.json` adoption verdict, the record of the space the\n command was asked about, keyed per space the way the pidfiles are. A re-signed credential and a\n broker-accepted adoption stay distinct facts. A root-only `renewal.json` left by an older build\n names no space and is never read as any space's verdict (`doctor auth` names it as a leftover).\n- **web**: local PID record, then the `/api/meta` response at the address the dashboard recorded\n once it was listening, which must name the same PID. The holder of `web.pid` picks the record, as\n on the `Web process` row: a service-run dashboard's own session record, found through its web\n service instance and named on the row as `service instance <id>`, or `web.session` for any other\n holder. The probe presents the readiness nonce recorded beside that address, the one credential\n the dashboard accepts on `/api/meta`. The probe follows no redirect. An answer with a status\n outside 2xx, a 3xx included, is `not-serving`, and the row names that status, such as `HTTP 401`.\n A live PID with no recorded address (the dashboard is still writing it, or an earlier build\n started it), or an unrecognizable process record, is `refused`, not a green default-port guess. A\n session record or service instance record that exists but cannot be read is also `refused`, and\n the row names the read error.\n- **broker**: the registered mesh URL dialed from this host with its recorded TLS requirement.\n\n`absent` means Cotal has no live local component record (or has a stale record); `not-serving`\nmeans the component record is live but its service/readiness surface did not answer or is not ready.\nThose are intentionally separate exit cases. A failed or unreadable probe is `refused`, never an\nabsent component or a clean zero. A PID record that exists but cannot be read, or exists under both\nits current and a pre-upgrade name, refuses only its own row. A record that its component removes\nwhile the pass runs reads as `absent`.\n\nEach component prints on one line. A control character inside a reported reason, such as a newline\nin a stored row's field name, prints as its `\\uXXXX` escape.\n\n## spawn\n\n```bash\ncotal spawn [<persona>] [--file <persona-file>] [-d | --foreground] [--dry-run] [options]\n```\n\nA bare `spawn` launches through the running manager, waits until the agent joins, and attaches this\nterminal to it (see [Managed launch view](#managed-launch-view)). It needs usable input and output\nterminals and refuses `--json` and `--plain`, before an identity is allocated. `--detach` (`-d`)\nreturns once the agent has joined, without attaching. `--foreground` runs the application directly\nin this process. `--file` (`-f`) names a persona file, never a manifest: manifests deploy with\n`cotal team deploy <file>`, and a manifest passed to `--file` is refused with that command.\n\nOn a user-auth mesh, an ordinary `--foreground` spawn without `--name` allocates the first free actor\nin the persona's series: `default_agent`, `default_agent_2`, `default_agent_3`, and so on. Held\nlocal material and existing grants are occupied, even when no presence is visible. Allocation\npreserves the existing session and its credentials. An explicit `--name`, a signed enrollment,\nand a managed handoff pin an identity and refuse a collision instead of renaming it. Authorization,\nnetwork and malformed-provisioning failures are not reasons to try another name.\n\nFor a foreground spawn onto a remote user-auth mesh, a one-time enrollment may stand in for a cached\nhuman login. Prefer a private file:\n\n```bash\ncotal spawn --foreground --file ./seat.md --space main \\\n --enrollment-file /run/secrets/cotal-enrollment\n```\n\nThe file contains only the enrollment URL, ending with at most one line terminator, and must be\nmode `0600` on POSIX. A launcher may name the same file in `COTAL_ENROLLMENT_FILE` instead, and run\n`cotal spawn --config ./seat.md --space main`. An orchestrator that cannot mount a file may set\n`COTAL_ENROLLMENT_URL`; that value is redeemed byte for byte, so a trailing newline in it is refused.\nNaming more than one source is refused, and so is `--enrollment-file` with `--detach`, `--creds` or\n`--on`. Enrollment input requires `--space` and applies only to a foreground persona spawn. If the\nmesh is not registered yet, the enrollment response must carry the stock user-bundle fields and the\ncommand needs a persona file (`--file`, or `--config` from a launcher) because there is no local\nremote-mesh persona catalog to read. The client\nredeems the URL once, registers the returned mesh material, exchanges the returned actor token at the\npinned auth service, and removes both enrollment variables before starting any child process. A\nregistered mesh whose record pins no exchange URL is refused before the URL is redeemed, so the\nenrollment stays unspent; re-register the mesh with `cotal meshes add <space> --from <url> --mode user`.\n\nA cached login for the same IdP and an enrollment are conflicting proofs, so the command refuses\nrather than choosing one. An invalid enrollment never falls back to login provisioning. Unknown,\nexpired, revoked, and already-used enrollments all produce one response: ask the owner for a fresh\none. See [Enrollment redeem](identity-and-auth.md#enrollment-redeem) for the HTTP contract.\n\nA runtime that starts a managed seat outside the manager's filesystem hands the child a managed\nhandoff instead: one `0600` file named by `COTAL_MANAGED_HANDOFF_FILE`, carrying the lifecycle the\nmanager already enrolled. The runtime builds the command with `delegatedSeatCommand`:\n\n```bash\nCOTAL_MANAGED_HANDOFF_FILE=/run/seat/handoff.json \\\n cotal spawn --config ./seat.md --space main --name <actor> --agent claude \\\n --expect-owner <owner> --expect-lifecycle-uid <uid>\n```\n\nThe `cotal` entry reads the file, deletes it and drops the variable before it parses flags, prints\nhelp or loads extensions, so every outcome leaves no file. The variable is read under any letter\ncase; spellings that name different files are refused after every one of them was deleted. The\nspawn then refuses a malformed handoff, or one whose space, owner, actor or lifecycle UID differs\nfrom `--space`, `--expect-owner`, `--name` and `--expect-lifecycle-uid`, before any broker\nconnection or exchange request. Every refusal on this path names the field and never a value from\nthe handoff. The registration's server, exchange and enforcement checks, the local state this\nmachine keeps for the space (its mesh record, user-auth state and agent secret files), target\nresolution, the policy refresh, the broker preflight and the agent auth preflight quote the space,\nthe server, the exchange URL, the actor or a path named for one of them in their own diagnostics and\nin the filesystem errors under them. For a handoff each prints one fixed sentence that names the\nfield and the phase instead, whether its check fails or an error is thrown. When the agent auth\npreflight's rollback then fails to remove a secret or file, that sentence is followed by the names of\nthe cleanup steps that failed, without their errors. The event-plane policy\nrefusals name the handoff's space field. An actor outside `[A-Za-z0-9_]` and a space that cannot\nname local state, such as `..`, are refused as malformed before any plane. A handoff conflicts with\nthe enrollment variables, `--detach`, `-f` and `--creds`, and needs `--config <persona-file>`. From\nthere it runs the enrollment consumer above without redeeming anything. See\n[Delegated seats](embedding.md#delegated-seats-outside-the-managers-filesystem).\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | resolved mesh | Target space |\n| `--server <url>` | registry entry | Broker URL override |\n| `--creds <path>` | none | Control-caller creds for an off-registry manager. Managed launches only; refused with `--foreground` and from a managed seat's own shell |\n| `--name <n>`, `-n` | persona's `name:` | Presence-name override (does not choose the persona) |\n| `--file <persona-file>`, `-f` | none | Persona file to launch, on the execution host. A value with no path separator that does not end in `.md` is read as `./<value>`. Refused together with a positional persona, and when the file is a Mesh manifest |\n| `--config <persona-or-path>` | none | Reserved for internal launchers that set `COTAL_MANAGED_HANDOFF_FILE`, `COTAL_ENROLLMENT_FILE` or `COTAL_ENROLLMENT_URL`; any other use is refused and names `--file` |\n| `--harness <connector>`, `-H` | persona's `agent:`, else the [`harness` preference](config.md#preferences), else `claude` | Connector type (`claude`, `opencode`, `jcode`, `hermes`, and so on). `--agent` is the legacy spelling; giving both is refused |\n| `--role <r>` | persona's `role:` | Role override |\n| `--model <m>`, `-m` | persona's `model:` | Model override |\n| `--variant <v>` | persona's `variant:` | Model variant override (connector-defined; e.g. OpenCode reasoning tiers) |\n| `--cwd <dir>`, `-C` | the serving manager's workspace root; with `--foreground`, this cwd | Working directory to root the agent at. On a managed launch, a relative path resolves against the manager's workspace root, not your shell, and a directory that does not exist on the serving manager's host is refused before launch. |\n| `--prompt <text>`, `-p` | none | Initial prompt auto-submitted at start |\n| `--opt <k=v>` | none | Connector-specific launch option, repeatable. Wins per key over the persona's `launchOptions:`; a connector with no option surface refuses it |\n| `--resume <id>` | none | Fork an existing session id into the mesh; only connectors that declare resume support accept it (see [the matrix](connectors.md)). The manager records the source session id, and `ps --wide` shows it. On a managed launch with `--on <instance>`, a Claude session held on this host is carried to that instance first ([Resume a session](connect-claude.md#resume-a-session)); carrying one needs `--on` |\n| `--no-events` | event plane on where supported | Opt out of the session's structured event plane (`--events` only restates the default) |\n| `--share-tools <sel>` | none | Share named operator MCP servers with the agent |\n| `--subscribe <a,b>` | persona's | Channel read-set override; `\"\"` clears it |\n| `--allow-subscribe <a,b>` | = subscribe | Read-ACL override; `\"\"` sets it to the read set |\n| `--allow-publish <a,b>` | deny | Post-ACL override; `\"\"` clears it |\n| `--detach`, `-d` | off | Launch through the manager, wait until the agent joins, and return without attaching (reattach with `cotal attach`) |\n| `--foreground` | off | Run the application directly in this process. Its exit, or the terminal's, may end the agent. Refuses `--detach`, `--creds`, `--on`, `--restarts`, and `--json` or `--plain` except with `--dry-run` |\n| `--on <instance>` | class anycast | Managed launches only: pin the launch to one manager instance id (the whole id, as `ps` prints it). Refused with `--foreground` (no manager to pin) and when empty |\n| `--restarts <n>`, `--restart-window <ms>` | none | Managed launches only, and given together: the manager restarts the agent at most `<n>` times within `<ms>`. Each must be a positive integer |\n| `--live-only` | off | `--foreground` only: skip the durable backstop |\n| `--enrollment-file <private-file>` | none | With `--foreground` and `--space`: a private file holding one seat's enrollment URL (see above) |\n| `--dry-run` | off | Preview the launch without allocating an identity, provisioning anything or starting an application |\n| `--expect-owner <u_\u2026>` | none | With `COTAL_MANAGED_HANDOFF_FILE` only, and required there: the owner the handoff must carry |\n| `--expect-lifecycle-uid <uid>` | none | With `COTAL_MANAGED_HANDOFF_FILE` only, and required there: the lifecycle UID the handoff must carry |\n\n`spawn` refuses `--runtime` and `--allow-stale` with \"manifest deployment options belong to cotal\nteam deploy <file>, not spawn\".\n\nEach session uses its connector's **event plane** by default: a stream of structured events\ndescribing what the agent did, rather than the prose it wrote, on a channel of its own. The channel is named after\nthe agent's principal, `events.<owner>.<actor>`, never after its display name, because two live\nagents are allowed to share a display name and would then share a stream. The launch grants publish\nrights on that channel alone, foreground and detached alike. On an open mesh, which issues no\ncredentials, the launch still allocates the agent an id, so the channel names a stable actor.\n`--no-events` is the explicit opt-out unless the selected registration says\n`policy: { events: \"required\" }`. Required policy makes the\nevent arm and grant mandatory, so `--no-events` and connectors without an event plane are refused.\n\nThe launch decision and the grant are separate on purpose. Holding publish rights on a channel is\nnot a request to publish to it, so writing an event channel into an agent file's `allowPublish`\ndoes not override `--no-events`.\n\nThe persona (`--file` or the positional name, else the [`persona` preference](config.md#preferences),\nelse `default`) is loaded from the target mesh's `.cotal/agents/` when it is a bare name. A reference that contains a path separator or\nends in `.md` is loaded from that file. A relative path resolves against the mesh root, except that\na managed handoff, and an enrollment onto a mesh this machine has not registered yet, resolve the\npersona file against the working directory. A missing\npersona is refused with the catalog directory or the file that was checked. A missing `default`\npersona also names the command that writes one: `cotal up` in the root of a space this machine\nstarted, or `cotal persona create default` for any other space. The launch flags\noverride the file. On a user-auth mesh the\neffective name is also the agent's actor token, so it must match the token grammar (no `-`); the\nspawn is refused with that explanation before any request is sent. `--foreground` runs the agent\nattached to your terminal; a bare spawn and `--detach` hand the launch to the running manager. Every\nmode gets the durable backstop on a mesh that runs the delivery daemon; `--live-only` skips it for a\n`--foreground` spawn (messages posted while it is disconnected are then not replayed). A foreground exit retires\nthe agent's creds and broker footprint, like a manager despawn. On a static-auth mesh, a spawn that\nfails after its footprint exists, for example because the creds directory cannot be written,\nretires that footprint the same way before it exits with the error. The creds and the footprint are\nremoved as separate steps, so a creds file that cannot be deleted does not leave the broker durables\nbehind, and the failed steps are reported together. On a user-auth mesh the two arms\ndiffer: a spawn against a mesh this machine provisioned revokes the actor row on exit, while a\nremote spawn (an enrollment or the advertised provisioning endpoint) removes only this machine's\ncredential files; its grant stays until the mesh operator revokes it, and the launch line says\nwhich arm you are on. A spawn through the advertised provisioning endpoint against a record that\npins no exchange URL is refused before the grant is requested, so no credential lands on this\nmachine. With no sign-in the spawn is refused with `AUTH_REQUIRED` (exit 2) before any request is\nsent. Any provisioning failure after the request, an endpoint 401 or 403 included, is reported as\n`OPERATION_UNKNOWN` (exit 3): the endpoint belongs to the deployment, and nothing says what its\nrefusal left behind. A foreground `--share-tools` selection that names a server the config does not declare is\nrefused before anything is provisioned. A managed spawn is an\n**action**: the manager accepts it and allocates the identity, and the CLI waits for a real outcome\n(the agent joins, exits, or about 30 seconds pass) before it reports or attaches (see\n[the control surface](control-surface.md)). See [Connect Claude Code](connect-claude.md) and\n[Agent files](agent-files.md). (`cotal start` was merged into `cotal spawn --detach`.)\nA managed spawn onto a manager from another Cotal release is refused before any request is sent\nwhen the manager's contract does not declare a field this CLI sends. The refusal names the field,\ncalls it version skew, and gives this CLI's version. A field you leave unset is not sent, so it\nnever causes that refusal.\n\nA manager has 50 seat slots, and each seat counts once. A slot is held by a managed seat (a row in\nthat manager's `cotal ps`, including a seat still joining), by a reserved launch the manager accepted\nbut has not started a process for, or by a cooling hold. A seat that ends within 10 seconds of\nstarting leaves its slot cooling until those 10 seconds pass, unless an operator stopped it. Such a\nseat holds only that cooling slot, even while its launch is still reporting the failure. A spawn\nrefused at the limit states that split and whether waiting can free a slot:\n\n```text\nat capacity (50 of 50 slots: 49 managed, 0 reserved, 1 cooling); waiting frees a cooling slot in 7s, or despawn one\n```\n\nA cooling slot frees at the stated time. A launch that has not settled frees its slot only if it\nfails, and a managed seat frees its slot only when it stops. The refusal counts a launch as pending\nonly while it holds a slot, so a launch whose seat already ended is not counted. The roster counts\npresence, which also includes peers no manager owns, so its total is a different number.\n\nRun from a managed seat's own shell on a static or open mesh, a managed `cotal spawn` launches as\nthat seat when it targets the seat's own space. Without `--space` it picks that target the way the\noperator path does, so a recorded mesh that is not running is skipped. The CLI reads the seat's\nlaunch identity (`COTAL_NAME`, `COTAL_ID`, `COTAL_LIFECYCLE_UID`, `COTAL_SPACE`, and on a static\nmesh the seat's own credential), so the manager records the seat as the spawner, the same as for\nthe seat's `cotal_spawn` tool. On a static mesh that credential also proves the seat's space, so a\nlaunch without `COTAL_SPACE` still runs as the seat, and a target space holding no credential for\nthe seat is refused. An open mesh acts as the seat only when `COTAL_SPACE` names its space. The\nseat can then stop the child with `cotal_despawn`, and the manager stops the child when the seat\nexits. On a static mesh a seat whose agent file lacks `capabilities: [spawn]` is refused, because\nits credential holds no spawn subject.\n`--on <instance>` keeps its pin: the seat's own credential has no instance route, so on a static\nmesh the CLI mints a one-shot `manager-caller` view for the seat, pinned to that instance and\ncarrying the spawn subject only when the seat's credential holds it. On an open mesh the call keeps\nthe TLS requirement the mesh records. A managed spawn from a seat's shell never falls back to the\noperator: `--creds` is refused there, and so is a target the seat cannot act on as itself, such as\n`--server` with an unregistered `--space`, a user-auth mesh, or an open mesh that `COTAL_SPACE` does\nnot name.\n\nForeground user-mode launches serialize their local actor material. A duplicate refuses\nwithout replacing the first launch's files. A new local-user foreground launch also refuses\nan existing grant before rotating it. Departure cleanup is conditional on the launch's token\nand lifecycle generation, so an earlier launch cannot revoke a successor's grant. Remote\nprovisioning remains the mesh operator's authority and can reissue material before the\nclient observes an existing same-host consumer.\n\n## models\n\n```bash\ncotal models [--json | --plain] [-H | --harness <connector>] [--refresh] [--on <manager-instance>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--harness <connector>`, `-H` | all registered applications | Application whose catalog to list |\n| `--on <manager-instance>` | native selection | Ask only the named manager instance. An unavailable or unauthorized pin refuses without choosing another. |\n| `--refresh` | off | Ask the connector to refresh its provider cache |\n| `--json` / `-j` | off | One JSON document: `result.items` holds each catalog as returned; the requested manager ID, answering ID and answering epoch are separate |\n| `--plain` | off | The same report without color |\n\nAsks the selected manager for each application's model catalog (model ids plus their variants)\nfor connectors that expose one. OpenCode and Codex query harness/provider surfaces; Jcode reads\nproviders that enable `model_catalog = true` in the operator Jcode `config.toml`. Jcode's listed\neffort tiers render as `<tiers> (declared, not provider-verified)`, and launch can still refuse one.\nA connector without a catalog says so with `supported: false`, and one whose install has no\ncatalog it can list, such as OpenCode 2.x, gives the reason in `unavailable`; neither is a failed\nread. A connector whose catalog could not be read, or that the manager could not load, makes the\nread `partial` (exit 4) with one `MODEL_CATALOG_UNREADABLE` warning naming its cause. The\nscreen points to that warning without repeating it inline. The remedy is to correct the stated\ncause before retrying; a refresh cannot repair missing connector configuration. A manager-level\nrefusal is reported separately. A connector whose harness the manager did not find at boot\nreports the reason boot recorded, as a spawn does, so restart the manager after installing it. Pick\na result with `cotal spawn --model <id> --variant <v>`, where `<id>` is the model id as the catalog\nprinted it. OpenCode and Codex ids are the full\n`provider/model`; Jcode ids are bare (`opus-5`, not `cliproxy/opus-5`), because the provider is\nselected by the operator's Jcode config and a prefixed id is refused at launch with the bare form\nnamed.\n\n## endpoints\n\n```bash\ncotal endpoints [--space <s>] [--server <url>] [--creds <path>]\n```\n\nLists the mesh presence roster: agents, the manager, and any other protocol endpoint, with each\nendpoint's role, kind, status, and current activity. Unlike `ps`, this is a read-only presence view;\nit is not limited to child processes owned by the manager.\n\n## who\n\n```bash\ncotal who [--waiting] [--all] [--json | --plain] [--space <s>] [--server <url>] [--creds <path>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--waiting` | off | Only rows that are waiting or report a condition |\n| `--all` | off | Also list infrastructure endpoints such as the manager |\n| `--json` / `-j` | off | One JSON document: `result.items` holds one row per participant |\n| `--plain` | off | The same report without color or marks |\n\nReads the live presence roster of the selected space. Each row has the participant's name, role, kind, status and condition,\nactivity, attention mode, harness, model and host, as its presence reports them. Infrastructure\nendpoints are hidden unless `--all` asks for them; the report says how many it hid.\n\n`managed` says whether a manager runs the seat. With no manager on the roster it is `false` for\nevery row and no manager is asked. Otherwise every registered manager is asked for its seats, and a\nseat is matched on its lifecycle UID. In a user-auth space the manager answers only for the\ncaller's own seats, so other seats show `managed: null` with a `MANAGED_SCOPE` warning. An\nidentity the broker does not let ask the managers, such as a user whose grant has no `admin`, sees\nevery seat `null` with a `MANAGERS_DENIED` warning. These two warnings say how far the identity's\ngrant reaches; the roster itself was read in full, so they do not make the run partial. A manager\nthat does not answer leaves every unmatched seat `null` with a warning.\n\nA part `who` could not read makes the run `partial` (exit 4): a roster snapshot that did not finish\n(`ROSTER_INCOMPLETE`), a roster that has gone stale (`ROSTER_STALE`), an error on the roster watch\n(`ROSTER_ERROR`), or a manager census it could not read in full (`MANAGERS_UNREAD`, and the warnings\n`ps` names). A credential that may not read the roster is refused with `PERMISSION_DENIED` (exit 2),\nnever shown an empty roster.\n\n`who` watches the roster and the channel registry on every mesh so it can report those states. On a\nstatic-auth or user-auth mesh the credential `who` connects with may not delete its watch consumers,\nso each one stays on the broker for five minutes after `who` exits. A script that runs `who` in a\ntight loop there can reach the broker's limit of 1000 consumers per stream.\n\n## whoami\n\n```bash\ncotal whoami [--json | --plain] [--space <s>] [--server <url>]\n```\n\nReports the identity this CLI acts under in the selected space and what it may read, post and do.\nIt reads the mesh registry, the login cache and, when it is on this machine, the user-auth grant\nledger. It does not refresh the remote space catalog or contact the IdP. A one-shot command has no\nsession, so there is no unread count or connection state to report.\n\n| Mesh | `identity.kind` | What it may do |\n|---|---|---|\n| Open | `open` | Everything; the broker authenticates nothing |\n| Static auth, signer on this machine | `space-signer` | Everything; each command mints its own short-lived credential for its task, so there is no standing principal |\n| Static auth, no signer here | `no-credential` | Nothing without `--creds <file>` |\n| User auth | `user` | The signed-in user's `cli` actor: `read`, `post` and `capabilities` are its ledger row |\n\nA user who is not signed in has no principal. A signed-in user with no ledger row (`grant:\n\"not-granted\"`) may read and post nothing. When the grant ledger is on another machine the grant\ncannot be read here: `read`, `post` and `capabilities` are `null`, the run is `partial` (exit 4) and\nthe `GRANT_NOT_LOCAL` warning says so. A remote registration can hold a cached login without the\nsecret that derives its owner and principal. Those fields stay `null`; the report calls the\nprincipal unknown on this machine, rather than calling the user signed out. The cached expiry is\nadvisory and the command does not verify the login with the IdP. `dms` says whether the identity\nmay read DM history: on an open mesh, or with the space signer's audit credential on an authenticated mesh. The report\nreads local configuration and does not contact the broker to check its authentication requirements.\nAn unregistered `--space` is refused even with `--server`. Server addresses in the identity report\nand connection refusals omit URL user information, query strings and fragments, and escape terminal\ncontrols. Addresses need a host and a supported broker scheme. TCP addresses cannot have a path;\nWebSocket routes and bare host addresses remain supported. Invalid addresses make `whoami` refuse\nrather than print the raw value. Recovery instructions use a `<url>` placeholder rather than a\nsecret-stripped connection address.\n\nBoth reads refuse an unusable local mesh registry with `SCHEMA_INVALID` (exit 1) before\nconnecting. The diagnostic names the file without repeating its stored field values.\n\n`endpoint invoke <target> <command> --cast` uses the native one-way verb. Only an ephemeral\ncommand can be cast; journaled commands and `--wait` combinations refuse before publication.\nThe receipt records the original request, resolved binding and broker flush, not a responder or\nconfirmed execution. A class cast is not retried or repaired if another instance refuses its\nbinding. A broker publish denial remains a refusal. A lost publication outcome remains unknown.\n\n## service-endpoint\n\n```bash\ncotal service-endpoint approve <proposalId> [--caller-ceiling a,b | --request-id <hex32>] [--space <s>] [--json | --plain]\ncotal service-endpoint refuse <proposalId> [--request-id <hex32>] [--space <s>] [--json | --plain]\ncotal service-endpoint proposal <proposalId> | operation <operationId> | name <endpoint> [--space <s>]\ncotal service-endpoint name-bind <endpoint> (--owner <u_\u2026> --claim <ref> | --request-id <hex32>) [--space <s>]\n```\n\nDecides and inspects service endpoint registrations on a user-auth space whose auth service runs\nwith `--service-endpoints`. Every subcommand is one request to the service's\n`/service-endpoint-host` route as the signed-in user: the route verifies your IdP token and derives\nthe granter itself, so no command names one. `approve` and `refuse` are the owner's: the command\nreads the proposal and its name first and decides at those revisions, so a proposal or name that\nmoved in between is a conflict the host reports, never a decision on something else. `proposal`,\n`operation` and `name` read as the owner: another owner's record reads absent. `name-bind` is the\noperator's: it needs the auth service on this machine and runs on its loopback face under the\nexchange capability, with your signed-in subject as the audit id. The host verifies no domain\ncontrol, so `--claim` is your own reference for the namespace you checked. The answer is the host's\nown. A decision or a binding is recorded under its request id at\n`.cotal/auth/space.<key>/service-endpoint-requests/<id>.json` in this root before it is sent, and\nthe command prints that id, or names the exact resend command in the failure when no answer came\nback. A run interrupted before any answer prints nothing: the record's file name is the id.\nRun the same subcommand and subject again with `--request-id <hex32>` to send the recorded request as it was,\nwhich the host replays with the same approval or binding; a fresh read would carry the revisions\nthe first attempt moved, so the replay never rebuilds the request, and it takes no\n`--caller-ceiling`, `--owner` or `--claim`. An id this root never recorded, or one recorded for a\ndifferent command or subject, is refused with nothing sent. A run without `--request-id` is a new\nrequest, and a proposal already being approved answers `approval-in-progress`.\n\n## Handle inspection\n\n```bash\ncotal auth handle inspect <private-file> [--space <space>] [--json|--plain]\n```\n\nThis reads one protected native handle artifact or a bounded array of artifacts once. Without\n`--space`, it shows unverified public scope, holder and declared expiry claims. Signatures,\nissuer authority, parent links, holder currency and revocation remain not checked. No broker\nis contacted or credential issued. Signature bytes and original signed artifacts are never printed.\nThe human report shows at most 50 scope rows and names omissions; JSON retains public metadata.\n\nAn explicit `--space` selects online inspection on a registered static-auth space whose root\nholds matching native operator authority. One ephemeral provisioner reader is minted without\npersisting it. Its broker account and identity must match before leader-served signer and\nissuer-namespaced revocation reads. The inspector checks signatures, issuer scope, presented\nparent digests and containment. Every required sturdy-handle status must be explicitly unrevoked;\nmissing, malformed, unreadable or stale spec/status pairs fail closed. Reads share one bounded\nchain budget. No records are created or repaired, and no alternate signer or broker is tried.\n\nThese non-atomic observations are signature inspection, not a usable presentation context.\nDeclared expiry, presenter identity, holder/process currency and contract-dependent use authority\nremain separate. No holder credential is conferred and no handle is redeemed. Open and user-auth\nspaces refuse this static reader rather than borrowing an operator identity. Reader-close\nuncertainty is reported separately.\n\n## Account signer export\n\n```bash\ncotal auth signer --out <private-file> [--space <space>] [--force] [--json|--plain]\n```\n\nThis exports the selected space's existing data-account signer through the native stripped-signer\nformat. It does not create a caller, rotate a key, start a service or export the operator root.\nA cached login is not signing authority. The selected root must hold validated static trust;\nconflicting current-folder and selected-root account authorities refuse before publication.\n\n`--out` is required. Its parent must exist and pass the protected physical-directory checks.\nManaged `.cotal` and workstation state are not valid explicit export destinations. The file is private, and secret contents\nnever appear on stdout, in the receipt or in diagnostics. Keep this account-wide signer out of\nlogs and support bundles; it is not a least-privilege endpoint credential.\n\nAn existing path refuses by default. `--force` first inspects an owned private regular file and\naccepts only the stripped format for the same space and account. It refuses full trust bundles,\nother accounts, symlinks and concurrent changes. An identical export is a no-op. New files use\nnative create-only private publication; replacement uses the inspected snapshot under a writer\nlock. An unconfirmed publication remains unknown and is not retried. Platforms without the\nprotected file policy refuse rather than substituting a weaker writer.\n\n## Endpoint authoring\n\n```bash\ncotal endpoint init reviews [--dir <directory>] [--json|--plain]\ncotal endpoint check <contract-file> [--json|--plain]\n```\n\n`init` creates `contract.json` and an executable `handler.mjs` template. The default directory is\nthe supplied name; `--dir` chooses another one. Its parent must exist. Files are created\nexclusively, without following a pre-planted symlink or replacing an existing file. A partial\nwrite retains the named created material for review rather than deleting it automatically.\n\nA simple scaffold name such as `reviews` declares `com.example.reviews`. A full reverse-DNS name\nis kept. Choose a namespace you control before requesting serving authority. Scaffolding creates\nno service definition, reference, credential, registration or process. The handler accepts one\nJSON object with `command` and `args` on stdin and writes one JSON result on stdout.\n\nThe version-1 local contract file has `schemaVersion`, `endpoint` and `cluster`. The cluster has\nnative `urn`, `revision`, `attributes`, `commands` and `events` fields. Each command carries its\nnative declaration plus `input` and `output` schema bundles, each `{root, members?}`. The installed\nnative compiler derives their closure digests; authors do not hand-maintain a second digest next\nto an edited schema. `members` maps native digest references to supplied JSON documents. Unknown\nwrapper or command fields are refused. This file is local authoring input, not a new wire kind.\n\n`check` reads at most 1 MiB, validates the native cluster declaration, compiles every input/output\nwith the installed schema profile and reports the derived cluster closure. All schema references\nmust resolve inside their bundle; no URL or adjacent file is fetched. Attribute and event\nsemantics remain opaque to the native parser, so a file declaring them produces a partial result,\nexit 4, rather than a claim that those semantics were checked.\n\nNeither command contacts a broker, loads the handler, starts a service or proves serving\npermission. Authoring takes no space, broker, caller or invocation flags.\n\nServe the edited contract through the independent service controller:\n\n```bash\ncotal service add reviews --preset exec-endpoint --space <s> --root <absolute-root> --contract <absolute-contract.json> --handler <absolute-node-executable> --handler-args '[\"<absolute-handler.mjs>\"]'\ncotal service start reviews --space <s>\ncotal service status reviews --space <s>\ncotal service logs reviews --space <s>\ncotal service stop reviews --space <s>\n```\n\nThis preset supports Linux and an existing registered static-auth root. The executable is an\nabsolute protected regular file, with argv supplied separately. There is no shell, PATH lookup,\nimplicit installation, account switch or open/user-auth credential fallback. For a Node handler,\nselect the Node executable and pass the script path in `--handler-args`. That JSON array has at\nmost 64 fixed nonsecret strings and its serialized form has at most 8192 characters. The handler\nruns in the contract directory with an empty environment. It is trusted local code with the\noperator's OS permissions, not a sandbox; it must not daemonize or leave independent children.\n\nStart recompiles one protected contract snapshot and pins its cluster closure and the executable\nfile identity across the controller handoff. Changes before launch refuse; changes to the\nexecutable before a later call also refuse. Only ordinary untargeted ephemeral commands are\nsupported. Targeted, journal, trait, attribute and event declarations refuse before registration.\nThe host publishes the schema roots, members, closure manifests and cluster to the native\ncontract store, registers one fresh native instance, and uses the native issuance gate and\nbranded serve grant. The handler receives no signer, executor credential or broker environment.\n\nThe native serving boundary validates input before spawning and output before replying. Each\ncall receives one `command`/`args` JSON object on stdin and must return one UTF-8 JSON value on\nstdout with exit code zero. Calls and casts need a native deadline; the handler budget never\nexceeds it or 120 seconds. At most 32 handlers run concurrently. Input is bounded to 64 KiB,\nstdout to 256 KiB and discarded stderr to 64 KiB. Deadline, pipe, process, JSON and schema failures\ndo not replay effects. Handler payloads and stderr are not forwarded to service logs.\n\nInitialization is recorded only after the real serve subscriptions flush and the native ready\nstatus returns. Controller readiness remains a process-bound initialization observation, not an\nongoing endpoint health probe. Logs and private launch records name the full native instance;\nuse that instance for an attributed `endpoint describe` or `endpoint invoke`. Normal renewal\nuses the same issuer, identity and registration, with one immediate same-broker reconnect. An\nunconfirmed renewal withdraws the service rather than starting a replacement.\n\nStop first prevents new handlers, requests termination of its owned handler groups, waits for\nprocess/pipe closure, drains serving and closes the connection. Native deregistration pins the\noriginal registration revision and preserves successor or in-flight governance state. Private\ncreate-only intent, registration and removal records retain the original coordinates. A later\nlaunch of the same local instance refuses an unsettled or malformed attempt instead of silently\nrepeating it. Unconfirmed initialization or cleanup remains unknown; neither process disappearance\nnor an initialization receipt proves native cleanup. The separate `endpoint deregister` route\nretains its broker-gone and revision guards.\n\n## Endpoint control\n\n```bash\ncotal describe <endpoint> [--on <instance>] [--space <s>] [--server <url>] [--creds <path>]\ncotal invoke <endpoint> <command> [--args '<json>'] [--timeout <ms>] [--space <s>] [--server <url>] [--creds <path>]\ncotal invoke <endpoint> <command> --name <agent> [--admin] [--space <s>]\n```\n\nThe generic v0.4 service surface. `describe` resolves a registered endpoint's command set off the\nwire - the reserved `describe` command answers the registered contract digests, the schemas are\nfetched from the space's content-addressed contract store, recompiled, and verified against those\ndigests - and prints each command with its capability class and targeting shape. `--on <instance>`\npins `describe` to one manager instance's rail (the whole id, as `ps` prints it under its\n`manager <id>` headers), so an operator can read what that instance serves in a multi-manager space;\nunpinned, the class queue answers and the attribution line names whichever instance did. `invoke`\ncalls one command by name: `--args` is a JSON object validated against the fetched input schema\n*before*\npublish, and `--timeout` sets the reply deadline in milliseconds (default 10000). `--server` overrides the\nregistry's broker URL and `--creds` names a creds file for an off-registry connection. A targeted command takes `--name <agent>` (resolved to the agent's current principal through\n`inspect`) or `--self`. `--admin` uses the admin instrument credential, whose cross-agent reach rides\nthe operator-only `any` authorization mode. Neither command has compile-time knowledge of any\nendpoint's schemas - this is the same trust chain every built-in control command now uses. Needs an\nauth mesh: the manager registers its service on both static and per-user meshes (a signed-in user\nrides their bearer; each visible or invoked command still requires its existing grant, and cross-agent\nreach needs the `admin` scope). An open mesh has no service registry.\n\nOn a static-auth root the operator's own credential sends requests to the manager endpoint only, so\nthe broker refuses its `invoke` of any other endpoint before the call runs. When the call and the\nendpoint's whole command surface are untargeted synchronous ephemeral commands, the refusal names\n`cotal auth mint <name> --endpoint <endpoint> --space <space> --out <private-file>`, with the space\nthe call resolved and the `--server` it was given, if any, so the caller is issued for the space that\nrefused even when another space is current. Run the same `invoke` again with `--creds <private-file>`. An `invoke --on <instance>` of another endpoint is refused the same\nway and names no remedy: with `--on` the operator's credential still carries manager rows only, and\na paired caller file takes no instance pin.\n\n## Managed seats\n\n```bash\ncotal ps [--json | --plain] [--on <instance>] [--wide] [--slots] [--space <s>]\ncotal stop --name <n> [--on <instance>] [--space <s>]\ncotal attach --name <n> [--on <instance>] [--no-reconnect] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--name <n>` | none | Managed agent to stop / attach (required) |\n| `--on <instance>` | class anycast (`ps`: class scatter) | Pin to one manager instance id (multi-manager space); takes the whole id as `ps` prints it, not a prefix. An empty value (`--on \"\"`, an unset shell variable) is refused, never treated as absent. A roster principal id (`local.\u2026`) is refused with a message naming the instance id `ps` prints |\n| `--wide` (`ps`) | off | Print each seat as a labeled record that adds the operational facts the manager records: the provider the connector reported serving the model, `cwd`, `pid`, spawner, lifecycle uid, the owning manager's instance id and host, and for a `--resume` seat the session it forked (`forked from <id>`, with the source title and transcript SHA-256 once a Hermes or Jcode seat has recorded its fork; a carried Claude session prints `forked from <host>:<id>` with its title, SHA-256 and `carried <time>`, the time its bytes reached the manager). Model and requested variant stay in the identity row rather than printing twice. A fact the manager did not record (for example a runtime with no real process, or a connector that reported no provider) prints nothing, never a placeholder |\n| `--json` / `-j` (`ps`) | off | One JSON document (see below). Mutually exclusive with `--wide` and `--plain` |\n| `--plain` (`ps`) | off | The same report without color or marks |\n| `--slots` (`ps`) | off | List the durable static slot rows this manager owns instead of live seats, through the `slots` command. Mutually exclusive with `--wide`. A row that is not in the live roster still prints, with `LIVE` false; a retired row never prints |\n| `--no-reconnect` (`attach`) | off | End the attach when its session ends, instead of re-establishing it. For scripts that want one run and one exit code |\n\nOn a controller stack started with `--no-manager`, `ps` refuses `TARGET_UNAVAILABLE`: no manager\nruns, so there are no managed agents to list. Bare `cotal up` adds the manager.\n\nA raw `--creds` file is refused by `ps`, `stop`, `attach` and the other control commands, because\nthat route mints no endpoint-caller triple; the project folder, or `--space` against the registry\nentry, is the route that does.\n\nThe human `ps` report is presentation text and is not a stable parsing target. Scripts use `--json`,\none envelope whose `result` is:\n\n| Field | Meaning |\n|---|---|\n| `kind` | `seats`, or `slots` with `--slots` |\n| `items` | Each seat (or slot) row as its manager returned it, unchanged |\n| `managers` | Each manager instance asked: `{instanceId, state}`, where `state` is `answered`, `silent` (registered, no answer within the deadline, with the `liveness` this command established), `refused` (with its `error` and `code`) or `contract-mismatch` |\n\n`target` is `{space, scope: \"all-managers\"}` for the class scatter, and\n`{space, scope: \"manager\", managerInstanceId}` for `--on` or a user-auth mesh. A manager that is not\n`answered` makes the read `partial` (exit 4), with a `MANAGER_SILENT`, `MANAGER_REFUSED` or\n`MANAGER_CONTRACT_MISMATCH` warning naming it.\n\n`--slots --wide` is refused: `--slots` lists durable static slot rows, `--wide` prints live seat facts, and the two answer different questions. Across a multi-manager scatter, `--slots` prints each manager's rows under its own instance header, the same way the plain `ps` scatter does.\n\nA revision-32 manager also supports a bounded native `slots` query through\n`endpoint invoke manager slots --on <instance> --args '{\"names\":[\"worker\"],\"includeRetired\":true}'`.\nIt point-reads up to 100 distinct aliases and can return their retired slot/head and\ncleanup observations. The default `ps --slots` query is unchanged and excludes retired\nrows. Omitted aliases, live-map absence and stop acknowledgements are not retirement\nproof. Match the returned principal and lifecycle UID before interpreting any terminal.\n\nThese are operator clients over the running manager's control plane. The default row includes the\nconnector, model pin, optional requested variant, and runtime as operational descriptors for the\nmanaged row. They do not make a shared display name a unique protocol identity; use `--json` when\nunambiguous owner+actor attribution is required. An omitted variant means no override was requested;\nCotal does not invent an effective provider default it cannot observe. `ps` also prints two state\nfacts per managed agent, because they answer different questions: the process fact from the manager's\nown runtime handle (`running` with its uptime, or `exited` with how long it ran), and the mesh fact\nfrom the roster (`idle` / `working` / `waiting` / `mesh offline`, or `not in roster` when the seat has\nno presence row at all: a seat that has not joined yet, or one that never did). When the seat's\nconnector relays a harness-reported condition, the mesh fact carries its code and how long it has\nheld, so a seat whose turn died on a provider rate limit reads `waiting (rate_limit for 40m)` rather\nthan a bare `waiting`, and `--json` carries the whole `condition` object. When the connector reports\nthe seat's last work event (presence `activeAt`), the mesh fact ends with its age, such as\n`\xB7 active 3s ago`, and `--json` carries `activeAt`. A seat whose turn stopped advancing keeps\nheartbeating, so its presence row stays fresh and this age is what shows the stall. A seat can be\n`running` and `mesh offline` at once: the process is alive and its presence has lapsed. That row says\nhow long, as in `mesh offline for <age>` with an age such as `3.5h`, counted from the seat's last\npresence heartbeat, which `--json` carries as `offlineSince` (epoch ms). The age is read only from\nthe seat's own presence record, matched on its principal and lifecycle uid, so a same-named peer or\nan older lifecycle never dates it. The manager log names each managed seat that is offline on the\nmesh while its slot is held\n(`seat offline on the mesh: <name> - last heartbeat <time>; process <state>`), including one its\nwatch first sees offline after a reconnect, and each one that comes back\n(`seat back on the mesh: <name>`), so a watchdog that only checks process liveness has a line to\nact on. The manager does not reap or re-key such a seat. The mesh fact is only a verdict while the\nmanager's own presence watch is fresh: when that watch has been silent past the liveness window, or\nhas not replayed the bucket yet, every row prints `mesh unknown` with the reason instead (`--json`\ncarries it as `meshView: stale | unpopulated`), because `offline` and `not in roster` would then\ndescribe the manager's watch rather than the seat. The manager rebinds a watch that goes\nsilent under a live connection on its own, so `mesh unknown` normally clears within a liveness window.\nOn a user-auth mesh `ps` also renders each managed agent's last credential-refresh outcome, fail-closed.\n\n**Mode split (chosen up front, never try-scatter-then-degrade):**\n\n- **Static / open mesh.** Bare `ps` is a **class scatter**: it freezes the live manager class from\n the records registry, merges every registered instance's agents grouped and attributed per\n instance, and a non-answering instance is shown as `registered, no answer within the deadline`\n (never silently omitted). A refused list or a missing answer makes the census incomplete: rows\n from other instances remain visible, but `ps` prints an incomplete-census warning on stderr and\n exits 4 (`partial`), including with `--json`. Those rows are not a complete seat count. A contract\n mismatch prints one plain comparison of the requested and served input/output digest pairs and\n advises aligning manager versions. The no-answer label means only that the instance is registered\n and did not answer. It does not say the host is down, because a dead host never deregisters itself and a\n live one can be slow; if it is gone, deregister it.\n `--on <instance>` pins the read to one exact instance id instead. A wrong pin fails loud\n rather than falling through: a well-formed id that no live manager carries is reported as\n `manager instance <id> did not answer` (nothing else is asked), and a credential without that\n instance's rail is reported as refused by the broker, not as an unresponsive manager. A manager\n that answers with a refusal is shown with its own cause; \"no manager reachable\" is said only when\n nothing answered at all. If the scatter's own registry read fails (the freeze or the reconcile),\n `ps` says the manager registry could not be read rather than pronouncing on the managers, which\n may all be up.\n\n**The verdict is scoped to the endpoint rail the request rode.** An issued caller rides the\nversioned `ep.v1` rail, a separate subject space from the legacy `ep` rail, and an endpoint serves\nboth (SPEC 13.15). A manager older than the versioned rail serves `ep` alone, so it can be running,\nregistered and answering while an issued caller's request reaches nobody. Silence on `ep.v1` is\nreported as `no manager answered on the <rail> rail` with `ep.v1` as the rail, and names both causes\nit is consistent with: no manager running, or one older than the rail. The CLI cannot tell them\napart, because the service registry records no package version, so check whether a manager is\nrunning and, if it is, its version. The same scoping applies to `cotal run`'s hosted verbs, which\ndrop the `--local` suggestion there, since `--local` drives the run from the calling process and\nnames the caller as its answerer.\n\n**`stop` and `attach` route by seat locality.** A seat can only be stopped or attached by the\nmanager actually running it, and the class queue does not know which one that is. So on a\nstatic/open mesh both verbs first ask every registered instance which one hosts the named seat, then\naddress that instance directly. This happens by default; you do not need `--on`.\n\n`--on <instance>` remains the override, for when you already know where the seat lives or the\nlookup itself is degraded. On a **user-auth mesh**, the exchange selects one authorized manager\nfor a short-lived `manager-caller` view. `--on` requests a specific instance. Without it, a command\nrun from a managed seat's shell selects the manager that launched the seat (`COTAL_MANAGER_INSTANCE`),\nand any other caller's selection must be unique. Discovery and the command use that instance route.\nThe caller gains no registry read or scatter permission. An absent, ambiguous or unauthorized\nselection refuses before sending the command. With several of your managers live, the refusal lists\ntheir instance ids so you can pass one to `--on`. By default, when every manager you registered has\nexited, the refusal says to start one with [`supervise`](#supervise); the command is not sent to the local manager.\n\nA trusted hosted composition may opt into an [interactive local primary](embedding.md#interactive-manager-primary).\nUnder that policy, unpinned human control selects the genuine local Manager up front, even when your\nown remote Manager is live; an unavailable local Manager refuses without remote substitution. This\naffects all interactive Manager verbs, not only `ps`. Select your remote Manager explicitly for its\nspawn, run or terminal operations. Explicit pins and managed-seat launching bindings keep their rules.\nThere is no user flag for the policy, and stock `up` and recovery defaults do not enable it.\n\nA seat is reported as **not found** only when every reachable instance answered for itself. An\ninstance that stayed silent past the deadline, or that refused the read rather than answering, said\nnothing about which seats it hosts, so the seat may be running on it. That case reports that the\nlocation could not be established, names the instances that did not answer, and states outright\nthat it is not a report that the seat is gone. Read it as unknown and retry with\n`--on <instance>`; a retry loop that treats it as \"already gone\" stops looking for a seat that is\nstill running. A single manager cannot tell \"hosted elsewhere\" from \"does not exist\": it answers\n`not-found` for both, which is why the search asks all of them and why an incomplete search\nconcludes nothing.\n- **User-auth mesh.** `cotal ps` reports what **one** authorized manager knows about your agents\n (an instance-addressed read against its in-memory roster, owner-filtered). It does **not** report\n other manager instances or establish whether they are reachable. Completeness across a\n multi-manager user-auth space is not claimed.\n A manager that does not answer fails the command outright (`TARGET_UNAVAILABLE`, exit 2), rather\n than printing an empty list that could be read as \"no agents\". Your ledger row needs the `admin` scope to\n reach `ps` at all; `spawn` alone is refused by the broker (the ep tier boundary). A broker-refused\n read keeps that refusal and adds the documented scope requirement and `cotal auth grants` remedy.\n The broker does not identify whether the missing permission is for the route, command or target;\n the CLI does not substitute another caller or retry on a different route.\n\n`attach` streams and drives an agent's terminal on the `pty` runtime; detach with the escape key\n(Ctrl-] by default; see the [`detach-key` preference](config.md#preferences)). The key is recognised\nas the legacy control byte and as the kitty keyboard protocol and xterm modifyOtherKeys encodings of\nthe same press, so a terminal with either protocol enabled detaches too. It does so over a one-use, holder-bound\nmesh session ([SPEC](../SPEC.md) \xA713.6): the manager replies with a signed session grant (never a\n`127.0.0.1` URL), the CLI redeems it once over the broker, and the browser console (`cotal console`)\ndrives the same session. `stop` and `attach` need a running manager to talk to. On a static mesh\nthey are cross-agent admin operations. On a user-auth mesh, your own agents (any agent under your\nowner) need only the `spawn` scope; another owner's agent needs `admin` on your ledger row\n([identity & auth](identity-and-auth.md)). Launch detached agents with [`spawn --detach`](#spawn).\n\n**`attach` reconnects when the link dies.** A session lives on a network link, and a laptop that\nsleeps, a VPN that drops or a wifi handover kills it. When that happens `attach` prints\n`[cotal: connection lost, reconnecting]` on stderr and starts asking the manager for a new session:\na fresh grant, a fresh per-session credential, a fresh connection, so every attempt re-runs the same\nauthorization the first attach did. On success it prints `[cotal: reconnected]`, the manager repaints\nthe seat's current screen the way it does for any attach, and you carry on in the same terminal.\nRetries wait 1s, 2s, 5s, 10s, then 30s, for as long as the seat exists. The detach key is read the\nwhole time the loop runs, the waits and the attempts alike, so a reconnect never traps you: press it\nwhile a session is being established and the attach ends there, and a session that lands behind the\npress is handed back to the manager rather than left holding a slot. Everything else you type while\nthere is no session is dropped rather than queued, so keystrokes aimed at a terminal that turned out\nto be frozen, Ctrl-C included, are not delivered to the agent by a reconnect you did not know had\nhappened. That starts before the first session, not at the first reconnect: at a terminal, `attach`\nreads and drops what you type while it is still resolving the mesh, so a key struck at a prompt that\nhas not come up yet does not reach the agent when it does.\nThe terminal is in raw mode for the whole reconnect, including when the link died before the first\nsession finished opening, so the detach key works there too instead of echoing as `^]`.\n\nA **pipe** carries script input. For example, `printf 'ls\\n' | cotal attach --name web` is\nbuffered until the session opens. Buffering continues across reconnects, so\n`tail -f log | cotal attach --name web` does not lose the part of its feed written while the link was\ndown. Only a terminal gets the reader; `--no-reconnect` keeps the old behaviour on both.\n\nIt stops on its own when reconnecting cannot help, and says why: a manager that refuses the attach\nexits non-zero with the manager's own message, and a reconnect that finds the seat no longer there\n(despawned, or its agent exited while the link was down) exits cleanly with `seat <name> is gone`.\nIt stops the same way on a refusal the CLI raises itself, such as a broker that denies the request.\nA local connect refusal that retrying cannot fix, such as a static-auth mesh whose seed is now\nmissing, also exits non-zero with the refusal's own sentence. A broker that is still unreachable\nkeeps the loop trying in silence.\nA refusal that could still pass, such as a manager at its session ceiling, is relayed in the\nmanager's own words while the loop keeps trying, once per refusal rather than once per attempt.\nPressing the detach key ends the view with `Detached`. A remote terminal-session end, including\nprocess exit or manager shutdown, prints `Attach session ended`. Neither report claims the agent\nis still running or that its cleanup completed. Use `cotal ps` to check its current state.\n`--no-reconnect` restores the single-session behaviour.\n\nEach reconnect also hands the abandoned session back to the manager, over the first link that can\ncarry the message, so an attach that flaps does not eat the manager's session slots one outage at a\ntime. If that message never gets a link, the attach says so when it ends. The live-session ceiling\ndefaults to 64 concurrent sessions (`--max-sessions`); the browser console opens one session per\npane, so a dashboard over a large mesh should size for agents \xD7 panes. Hitting the ceiling refuses\nbefore a credential is minted and names `--max-sessions`.\n\nWhich mesh `attach` resolves also decides **how it redeems the grant**. On a registered open mesh\nthere is no local seed. The CLI connects bare, the same way other control commands already do, and\nthe session rail is the caller rail that a real open-mode connection already reaches. Telling the\noperator to re-register the root is false: the registered root is already the contract. On a\nstatic-auth mesh the grant is still redeemed by minting a short-lived\nsession-scoped credential from the seed at the root the mesh resolved to, never from a `.cotal`\nfound by walking up from whichever directory you happen to be standing in. The difference is not\nhypothetical: `~/.cotal` exists on every install because the mesh registry lives there, so a command\nrun anywhere under your home directory but outside a project used to mint from your home\ndirectory's trust and present it to a broker that trusts a different chain, which surfaced as a\nbare authorization failure that named nothing. A directory that does hold another chain for the\nsame space is now reported on the way past, and not obeyed:\n\n```text\n! this directory resolves to /Users/you, whose .cotal/auth holds a DIFFERENT trust chain for space \"team\".\n attach used /Users/you/projects/app, the root this mesh resolved to. The other one is not being used, and is worth a look.\n```\n\nWhen a **static-auth** mesh holds no seed at the resolved root, `attach` refuses and names what it\nresolved, the broker and the root, instead of describing a directory it did not use and instead of\ntaking the open-mode path. An authenticated registry entry with a missing seed is still\nauthenticated. On a USER-AUTH mesh `attach` reads no seed. It sends your login and the session grant to\nthe auth service, which issues a `session-caller` bearer only if your owner and actor hold that\nsession. The connection it opens expires with the session grant.\n\nTerminal bytes stream over the mesh; the manager's own HTTP/WS face serves the console. That endpoint binds\n**loopback by default**, so nothing is exposed by accident; `cotal up --host <addr>` passes its bind\naddress down, which is what lets you reach the browser console (`cotal console`) for an agent whose manager runs on another machine.\n`attach` does not use that face: it redeems a signed mesh session grant over the broker instead (see above), so it reaches a\nremote manager regardless of the bind address. A\nbare `cotal supervise` and an embedded manager stay machine-local. Set it directly with\n`supervise --console-host <host>`.\n\nThat address is **recorded on the mesh** and carried forward, because it is a decision rather than\nsomething later commands can work out for themselves (a broker dial address is not a manager bind\naddress). Every later manager launch for the same mesh reuses it, including a same-root `cotal up` repair\nand an adopted preserved or restored listener. A manager replacement\ndoes not quietly move a reachable attach face back to loopback. Passing `--host` again overrides it,\nso you can widen or narrow exposure whenever you like; a mesh that never asked stays loopback-only\nand records nothing.\n\nBecause that face mints terminal read and write authority for every managed agent's browser session, it is credentialed in two\ntiers. A mesh caller receives a **ticket** bound to the single agent the manager just authorized,\nsingle-use and short-lived, so one authorized attach can never be re-pointed at someone else's\nagent. The **console token** is the operator's own, reaches every agent, and is printed only to the\nmanager's output. The roster, the live feed, and the PTY stream all answer `401` without one; the\nstatic console shell is served openly, since it describes no agent.\n\n## input\n\n```bash\ncotal input --name <n> --text <text> [--no-enter] [--on <instance>] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--name <n>` | | Managed agent to type into (required) |\n| `--text <text>` | | The text to type, taken verbatim (required) |\n| `--no-enter` | off | Type the text and stop there, without pressing Enter |\n| `--on <instance>` | class anycast | Pin to one manager instance id using the same rules as [`attach`](#managed-seats) |\n\nTypes one line into a running agent's terminal, as if you had typed it there, and returns. This is\nthe half of [`attach`](#managed-seats) that a program wants: `attach` is a live stream that holds a\nsession open and expects a terminal on your side, so a script, a cron job or a web UI cannot use it\nto send a single line. `input` is one authorized call.\n\nWhat it is for is **harness commands**. A line beginning with `/` is not chat and not a message: it\nis something the agent's own harness handles, and the only way in is the keyboard.\n\n```bash\ncotal input --name reviewer --text \"/compact\" # ask the harness to compact its context\ncotal input --name reviewer --text \"/model opus\" # switch its model\ncotal input --name reviewer --text \"hold on that PR\" # ordinary typing works too\n```\n\n**Quoting.** `--text` takes a value, so a payload starting with `/` survives as written. A payload\nstarting with a dash needs the `=` form, because the shell-style `--text --foo` is ambiguous and is\nrefused rather than guessed:\n\n```bash\ncotal input --name reviewer --text=--verbose # dash-leading text: use --text=<value>\n```\n\nEnter is pressed by default, since a command typed but never submitted has not been delivered.\n`--no-enter` types the text and leaves it sitting at the prompt, which is how you stage a line and\nsend it later.\n\nNothing comes back but a `Terminal input written` receipt. It names the agent, space and manager,\nthe bytes the manager reported writing (counting the trailing carriage return) and whether Enter was\nrequested. It confirms the write to the terminal only. Whatever the agent does next shows up where\nits output already goes: the mesh, its transcript, or an `attach`.\n\nA write the manager cannot confirm, such as a terminal write that failed partway, exits 3 with\n`Outcome unconfirmed` and is not typed again. Part of the text may already be at the prompt, so check\nthe seat before you send it again.\n\n**This one is operator-only, and more narrowly than `stop` or `attach`.** Those two are granted to\nanything holding `spawn`, so an agent can stop and attach to seats under its own owner. `input` is\nnot: it is granted only to operator credentials, which on a user-auth mesh means your ledger row\nneeds the `admin` scope, the same scope [`ps`](#managed-seats) already needs there. The reason is\nthat a write into a terminal is control of whatever is running in it, and on a user-auth mesh the\nown-owner rule covers every seat under you, not only the ones you launched: a `spawn`-scoped agent\ncould otherwise type into a sibling it never started. Seat locality is still resolved for you.\n\nOnly the `pty` runtime can be typed into. The external terminal runtimes (`tmux`, `cmux`, `orca`,\n`herdr`) attach to a process they do not own, so they have no input stream for it and the command\nrefuses by name rather than dropping the keystroke.\n\n## persona\n\n```bash\ncotal persona [--json|--plain]\ncotal persona show <name> [--json|--plain]\ncotal persona create <name> --file <persona.md>\ncotal persona create <name> --prompt <text> --subscribe <a,b>\ncotal persona edit <name>\ncotal persona remove <name> --yes\n```\n\n`p` aliases `persona`; `persona rm` aliases `persona remove`. These commands manage the\nselected space's local `.cotal/agents` catalog without connecting, refreshing a remote catalog\nor seeding connectors. `--space` and `--server` select the same root used by launch. A prepared\nspace has an offline catalog before it is started. Failed target resolution never falls back\nto the current directory. Remote `--on` operations refuse as unsupported here.\n\nCreation accepts one of `--file/-f` (a complete [agent file](agent-files.md)), `--prompt`, or\n`--from` (body file, or `-` for explicit stdin). Complete files validate all supported fields,\nmust name the requested persona and keep their original bytes. They cannot be combined with\n`--role`, `--model` or `--subscribe`. Body input requires an explicit `--subscribe` declaration,\nincluding `--subscribe \"\"` for no channels, and accepts `--role` and `--model`.\nCreation refuses an existing definition. Mutation receipts contain names and paths, not bodies.\n\nEditing opens a private draft using the editor preference (`VISUAL`, then `EDITOR`, then saved\npreferences). A successful editor exit and valid definition are required before saving. A changed\nor removed source refuses with `CONFLICT` (exit 2), leaving the current definition intact. Local\nCLI create, edit and remove share a per-persona lock during comparison and commit, not while an\neditor or confirmation is open. Manager and external writes do not participate in that lock.\n`--json` and `--no-input` refuse editing before an editor opens.\n\nList, show and mutation receipts support one-document `--json` and line-oriented `--plain`.\nShow explicitly returns the full definition and source. List does not return the body or launch\noptions. A malformed entry produces a partial inventory (exit 4); an unreadable catalog refuses\nrather than reporting it empty. Remove asks with cancellation as the default in a terminal.\nPlain confirmation uses numbered choices. Scripts and JSON removal require `--yes/-y`.\n\nThe legacy `personas` parser remains available during migration: `list [-v] [--running]`,\n`show`, `edit`, `new` and `rm --force`. Its `new` requires `--subscribe`, reads `--from -` from\nstdin, and overwrites an existing persona only with `--force`. Its local writes share the same\ndraft validation and commit fence. Only its explicit `--running` overlay connects to the mesh.\n\n## supervise\n\n```bash\ncotal supervise [--execution runtime|none] [--runtime <name>] [--space <s>] [--server <url>] [--spawn <names>]\n```\n\n`supervise` is a deprecated entry kept for existing units, scripts and containers, and help and\ncompletion omit it. It accepts the same flags and prints a warning on every run naming the\n`manager` service preset and the release that removes it; see\n[deprecated daemon entries](UPGRADING.md#deprecated-daemon-entries). The manager that `cotal up`\nstarts runs the same code through a hidden entry and prints no warning.\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space to supervise. On a root that hosts several spaces, name one of them here |\n| `--server <url>` | hosting mesh, or matching registered mesh | Broker URL. A registered mesh supplies it when omitted; a different explicit value is refused before anything is dialed. |\n| `--runtime <name>` | `pty` | Agent runtime (`pty` built in; extension runtimes are explicit-only) |\n| `--execution runtime|none` | `runtime` | NONE is a genuine hosting user-auth control instance with no Runtime, connector inspection, workload or terminal execution |\n| `--console-port <n>` | none | Protocol-console port |\n| `--console-host <host>` | loopback | Bind host for the console endpoint. Loopback keeps it machine-local; `cotal up` passes the address it bound the broker to, which is what lets the browser console reach this manager from another machine. `cotal attach` does not use this face: it redeems a mesh session grant over the broker |\n| `--max-sessions <n>` | 64 | Live-session ceiling. Each console pane and each `cotal attach` is one session, so size for agents \xD7 panes, not agent count. A capacity refusal names this flag. `cotal up --max-sessions` records the same number on the mesh so a later `supervise` started by repair keeps it |\n| `--roster <file>` | none | Declarative roster to boot at startup. See [Roster files](define-a-team.md#roster-files) |\n| `--launch <spec>` | none | Resolved manifest launch spec (from `up -f` / `team boot`) |\n| `--spawn <names>` | none | Comma-separated personas to pre-spawn at startup |\n\n`--ws-port`, `--resume-attempt` and `--resume-commit-token` are passed by `cotal up` when it starts\nthe manager, for the console session client and for resuming preserved seats. They are not for\ndirect use.\n\nThe manager is the agent supervisor and control plane: it answers `spawn --detach`, `stop`, `ps`,\n`attach`, and the `cotal_*` manager tools. `cotal up --detach` starts one for you; run `supervise`\ndirectly to recover a dead manager or drive a custom runtime. Default runtime is `pty`; install an\noptional provider first (`cotal ext add @cotal-ai/orca`, `@cotal-ai/tmux`, `@cotal-ai/cmux`, or `@cotal-ai/herdr`) and\nselect it explicitly. A missing provider or app fails loudly; there is no fallback. See [Deploy](deploy.md).\nBoot inventory decides whether this process takes unpinned `spawn`/`launch` on the class rail:\nif every declared connector is unavailable, those commands stay on this instance rail only\n(`status` reports `classSpawn: false`). `describe` still answers on the class rail, so an\nunpinned spawn can bind-fence against a skip member; re-issue, or pin `--on`. A partial\ninventory keeps the class rail and names `--on` on a harness refusal, because sibling\ninventories are not readable from the serve credential. See [control surface](control-surface.md#instance-routing).\n\nUse `--execution none` only for a fresh control instance in an existing hosting user-auth root.\nIt requires that space's signing trust, provider pin and user-mode mesh record, without remote\nmanager authority. It rejects `--runtime`, startup workload flags and participant registrations\nbefore resolving a Runtime or connector. The mode needs no connector seeding. An existing\nunclassified identity or retained workload refuses admission; a recorded NONE instance cannot\nrestart as runtime. Do not delete identity or capability files to change modes.\n\nNONE retains native registration, status and the selected Manager's empty managed-seat ps.\nIt does not adopt foreground agents, roster peers or HTTP-hosted sessions. Workload operations\nstill need an explicitly selected execution-owning participant Manager and its existing authority.\nStarting a control instance does not enable an auth service's interactive local-primary policy.\nNONE also refuses model inspection and caller-driven persona, purge, transcript, preservation,\nresume, cancellation, turn-yield and self-stop writes. It starts no transcript receiver or sweep.\nIts native registration, lease, renewal, session-ledger and goal maintenance can still write.\n\nLocal runtime startup now records identity and execution mode during admission. A later failed\nauthentication or registration can leave those files classified as runtime. Invalid supervise\narguments refuse before connector seeding, with one error line rather than seeding first.\n\nOn a normal `SIGINT`/`SIGTERM`, the manager stops every seat and requires the selected runtime to\nprove the seat is gone before it releases the manager lease or service registration. A stop that\ncannot prove exit fails loud and keeps manager authority instead of reporting a clean shutdown while\nan orphan still holds broker rails. When the runtime refuses the stop itself, the shutdown fails at\nonce and its error names the refusal as `stop failed: <message>`. After an abrupt manager death, the same logical successor\nterminalizes only its own durable static slots, verify-evicts the predecessor's broker principal,\nrecords that result in the lifecycle's caller-readable audit detail, reaps the predecessor's seat\nprocess through the runtime's custody reference recorded on the slot (the pty runtime verifies the\nprocess start identity in its seat record, so a reused pid is never signalled), and only then\nretires the lifecycle and frees the alias. A seat whose spawn carried `supervise` with restarts left\nis restarted instead of retired: the successor reaps any seat the slot still names, relaunches it\nunder the same name, lifecycle uid, credential, persona and cwd, and counts the restart against the\npolicy. A seat on a runtime that can adopt a live seat (tmux) and still running when the successor\nstarts is adopted in place: the successor checks that the recorded tmux server, window and pane are\nthe ones the slot names and that the pane's process still runs, then manages that seat again under\nits existing credential, without a restart. Both paths read the record the manager writes to\n`.cotal/seat-records/<space>/<name>.json` once a supervised or adoptable seat is ready, rewrites at\nevery restart and credential renewal, and removes when the seat is freed. A spent budget, a missing\nrecord, an expired credential, a persona file or credential that no longer matches the record, or\nan adoptable seat whose pane has exited retires the lifecycle as above. A runtime that custodies its seats reserves that\nreference before it launches one, and the manager records it on the slot's first durable row, so a\nmanager that dies part-way through a spawn also leaves a seat its successor can address. A\nsame-lifecycle restart or a resume records the new seat's reference on the slot the same way, and\nwhen the slot does not take it the restart or resume fails and stops any seat it started, so the\nslot never names a seat that has already exited while its replacement runs. A resumed seat keeps\nits retained credentials, so the resume frees it only once its exit is proved; a seat whose stop\ncannot be proved stays managed, and the resume's error says so, naming a stop the runtime refused\nas `stop failed: <message>`. A spawn\nthat launched its seat and then failed is rolled back by the manager that launched it, and that\nrollback reaps the seat through the same reserved reference before the lifecycle retires. Missing or unverified broker evidence keeps the slot\nterminalizing, and so does a runtime that cannot reap by reference. While the name stays held, a\nsame-name spawn or a resume re-drives that teardown with the same recorded reference, so it reaps\nthe seat again or keeps the name held.\n\nA `meshes add --mode user` entry is a **participant** registration, not hosting authority. A\nparticipant may run `supervise` only when the host advertises the remote manager authority service\nand the signed-in actor has the dedicated `supervise` ledger scope. The CLI obtains the closed,\nloopback-only `manager-service` view; `spawn` and `admin` do not substitute for that scope. The\nhost issues the manager's public-nkey JWT material through its lifecycle-bound prepare \u2192 activate\n\u2192 renew protocol, never by handing the participant a signer or static provisioner credential.\nThe host also performs instance-scoped eviction and guarded gate reconciliation. A remote manager\nrefreshes its short-lived registration executor before clean deregistration, so a long-running\nprocess removes its service row on `SIGINT` or `SIGTERM`. After an unclean stop, the same instance\nverify-evicts its superseded family and advances the process epoch. If an abandoned frozen gate\nholds the manager governance slot, a different supervise-scoped manager asks the host to reconcile\nthat holder after a complete gone verdict, then retries its registration once.\n\nA remote supervise never uses local signing trust: with host-issued authority in hand, the\nmanager mints from that authority alone and consults local records only to refuse a conflict,\nnamely the supervised space's own trust records under the cwd root. A root that hosts another\nstatic space beside the sign-in is a normal configuration and is never read as this space's\ntrust.\n\nThe broker URL in the registry entry decides the transport. A remote broker is often published\nover a `wss://` edge rather than a raw `nats://` port, and `supervise` dials whichever scheme the\nrecord holds, starting with the manager-authority registration it runs before the manager exists.\nThe record also decides whether that registration requires TLS, so a participant never downgrades\nthe credential exchange to a plaintext connection the registry did not describe.\n\nWithout that advertised host service or scope, `supervise` refuses before it starts a manager.\nRun `cotal spawn` without `--detach` to launch a foreground agent, or ask the space host to enable\nthe authority service and grant `supervise` for detached agents. If a running remote manager loses\nrenewal, it reports degraded state and refuses unsafe new starts and restarts; live agents are not\nsilently replaced. Do not run `cotal down` or `cotal up` on a participant machine to repair this\ncondition.\n\n## daemon\n\n```bash\ncotal daemon serve [--daemon-home <dir>] [--json|--plain]\ncotal daemon status [--daemon-home <dir>] [--json|--plain]\ncotal daemon login --idp <auth base URL> [--client-id <id>] [--daemon-home <dir>] [--json|--plain]\ncotal daemon connect-check --space <s> [--daemon-home <dir>] [--json|--plain]\ncotal daemon register --space <s> [--daemon-home <dir>] [--json|--plain]\ncotal daemon service-connect --space <s> [--daemon-home <dir>] [--json|--plain]\ncotal daemon service-disconnect --space <s> [--daemon-home <dir>] [--json|--plain]\ncotal daemon service-status --space <s> [--daemon-home <dir>] [--json|--plain]\n```\n\nThis release serves a local endpoint for status, sign-in, a connection check, a registration request\nand the daemon's own service connection. It does not start a Manager, serve any command on its\nservice, connect to Cloud, or install a system service. It supports Linux only. On other\nplatforms, an unset `XDG_RUNTIME_DIR` produces\n`TARGET_UNAVAILABLE` (`NO_RUNTIME_DIR`); with it set, the platform check refuses `UNSUPPORTED`.\n\n`serve` runs in the foreground and prints `daemon ready at <socket>` once the endpoint is bound\nand checked. That success line is plain text; `--json` shapes its refusals. `status` prints the\nstate, version, protocol, process ID, start time and uptime. `state: \"ready\"` means only that the\nlocal control listener is bound and verified, not that a mesh or Cloud connection exists.\n\nThe endpoint is `$XDG_RUNTIME_DIR/cotal/daemon.sock`. Without `XDG_RUNTIME_DIR` both forms refuse\n`TARGET_UNAVAILABLE` (`NO_RUNTIME_DIR`). Both refuse `PERMISSION_DENIED` for a runtime directory\nthat is a symlink, not a directory, not owned by you, or not mode `0700`. `status` also refuses\n`PERMISSION_DENIED` for an endpoint entry that is not a socket, not yours, or not mode `0600`.\nThe daemon drops connections from other users. The kernel separates users, not processes of one\nuser: any process running as you can use the local controls, including sign-in and cancellation.\n\n`status` with no daemon running refuses `TARGET_UNAVAILABLE` (`NOT_RUNNING`). `serve` refuses\n`CONFLICT` (`ALREADY_RUNNING`) for any existing socket, including one left after a crash, and\n`PERMISSION_DENIED` (`SOCKET_NOT_SOCKET`) for a non-socket entry. It removes neither.\nSIGTERM and SIGINT shut down cleanly and remove only the daemon's own socket. SIGKILL or SIGHUP,\nincluding closing the terminal, leaves a stale socket. To recover, run `cotal daemon status`.\nIf it reports `NOT_RUNNING`, confirm no daemon process is running, then remove the named socket\npath by hand and start again.\n\n`--daemon-home <dir>` runs or reaches a separate daemon whose own state lives in `<dir>`: its\nsign-in session, its per-space identity, register request and service store, all at the paths\nbelow with `<dir>` in place of `<COTAL_HOME>`. Spaces are still read from the shared Cotal home's\nmesh registry, which this daemon never writes. Its socket is\n`$XDG_RUNTIME_DIR/cotal/home-<16 hex>/daemon.sock`, derived from the directory, so it runs beside\nthe default daemon. `<dir>` must already exist as an absolute directory you own with mode `0700`,\nand cannot be the Cotal home. A relative path or the Cotal home refuses `USAGE_INVALID`, a missing\ndirectory `TARGET_NOT_FOUND`, and a symlinked, foreign or loose one `PERMISSION_DENIED`. Nothing is\ncreated or repaired.\n\n`serve` holds the lifetime lock `<dir>/daemon.lock` while it runs. A second `serve` for the same\ndirectory refuses `CONFLICT` (`LOCK_HELD`), naming the live holder. A start that fails for any other\nreason releases the lock again. A killed daemon leaves its socket behind, which still refuses\n`CONFLICT` (`ALREADY_RUNNING`) and is never removed, so recover as above. Every client command with\n`--daemon-home` sends nothing unless the process at that socket is the lock's live owner (same pid\nand process start). Otherwise it refuses `PERMISSION_DENIED` (`PEER_NOT_LOCK_OWNER`).\n\nThis daemon is a different machine identity. Nothing is copied from the default daemon: it needs\nits own `login`, its own `register`, and the space owner's approval of its own instance. Without\n`--daemon-home` every command behaves exactly as before.\n\n`login` signs the running daemon in with device approval. The daemon runs the same sign-in as\n`cotal login` and keeps the session in the machine's Cotal home, where `cotal login` keeps it. The\ncommand prints only the approval link, code and expiry to stderr, then the signed-in account. No\nsession token crosses the endpoint or reaches the terminal. `--idp` takes an HTTPS auth URL, or an\nHTTP loopback IP URL; `--client-id` defaults to `cotal-cli`.\n\nOne sign-in runs at a time: a second `login` refuses `CONFLICT` (`LOGIN_IN_PROGRESS`) without\ncontacting the provider. Ctrl-C, or the command's connection closing, requests cancellation on this\nmachine. If cancellation takes effect before persistence, nothing is saved, even if device approval\nalready happened. An already-completed attempt remains completed; cancellation revokes nothing\nat the provider. A cancelled attempt reports `USAGE_INVALID` (`CANCELLED`). A denied or expired\napproval refuses `PERMISSION_DENIED` (`DENIED` or `EXPIRED`), and an unreachable provider refuses\n`DEPENDENCY_UNAVAILABLE`\n(`UNREACHABLE`); none of these saves a session. Any other provider answer fails `OPERATION_FAILED`\n(`REFUSED`). Such a failure can come after the session was saved, so run `login` again. A failure\nis reported with a fixed sentence; the provider's own error text is never relayed. `status`\nkeeps answering while a sign-in waits.\n\n`connect-check` asks the running daemon whether it could register with a space as its own service.\nThe daemon is not a Manager and never borrows a Manager's identity or authority. The check reads\nthe space's registration on this machine and the daemon's sign-in, then the space's public account\nidentity from its auth service (`GET <exchange>/service-endpoint-authority`, no redirects). It sends\nno registration and holds no credential. A space whose auth service does not host service endpoints\nrefuses `UNSUPPORTED` (`UNSUPPORTED_BY_SPACE`). The daemon does not fall back to anything else.\n\nThe other refusals are `TARGET_NOT_FOUND` (`NOT_REGISTERED`) for a space this machine has not\nregistered, `UNSUPPORTED` (`NOT_USER_AUTH`) for a space without user sign-in, `AUTH_REQUIRED`\n(`NOT_SIGNED_IN`) when the machine is not signed in to the space's provider, and\n`DEPENDENCY_UNAVAILABLE` (`UNREACHABLE` or `REFUSED`) when the auth service cannot be reached or\nanswers with something unusable.\n\nOn success it reports `ready-to-register` with the space's account, the daemon's service name\n`ai.cotal.machine` and the daemon's own instance ID. Nothing has been registered. The space owner must\nstill approve the daemon, and the service name needs the space operator's binding. The first\nsuccess creates the daemon's identity for that space at\n`<COTAL_HOME>/daemon/<space segment>/service-identity.json` (mode `0600`). It holds the instance ID\nand the account, and no credential. Later checks reuse it. If the space later answers with a\ndifferent account, the check refuses `CONFLICT` (`ACCOUNT_MISMATCH`) and changes nothing: confirm\nthe space was not replaced before you remove that file by hand.\n\n`register` asks the running daemon to request its own service registration with a space. It first\nruns `connect-check` and returns any outcome other than `ready-to-register` unchanged, without\ncontacting the space again. Otherwise it sends one registration request for the daemon's instance\nwith this machine's sign-in, through the space's stock service authority. The daemon never sees or\nrelays that sign-in proof. The request ID is saved before the first send, at\n`<COTAL_HOME>/daemon/<space segment>/register-request.json`, and every later `register`, including\nafter a restart, replays the same request unchanged. The stock writer does not guarantee durability\nacross power loss.\n\nThe fixed service name binds to one owner per space. Different owners in a shared space need distinct\noperator-approved endpoint names; this command does not yet select one. The operator must verify or\ndelegate the namespace before binding it.\n\nThe space answers in one of three ways, each reported with exit 0:\n\n- `approval-required`: the space owner must approve this daemon and the space operator must bind its\n service name `ai.cotal.machine`.\n- `pending`: the space is processing an approved registration.\n- `registered`: the registration is recorded. It is not activated or serving, so the machine is\n still not connected.\n\nNo release ships a command or page that lets an owner approve the daemon or an operator bind its\nname. A refusal by the space is `PERMISSION_DENIED` with the space's reason as the native code, such\nas `PROPOSAL_REFUSED`. A reason the daemon does not recognize is reported as `UNRECOGNIZED`, and the\nspace's own text is never shown. A new proposal after a refusal needs a new request, which `register`\ndoes not send. A sign-in the provider rejects (expired or revoked) is `AUTH_REQUIRED`: run\n`cotal daemon login` again. A failure before anything was sent is `DEPENDENCY_UNAVAILABLE`. Only a\nrequest whose outcome is unconfirmed is `OPERATION_UNKNOWN`: run the same command again\nto replay it. A changed registration for the same instance is refused `CONFLICT` and nothing is sent.\n\n`service-connect` connects the running daemon to the space as its own registered service. It replays\nthe saved register request unchanged to read its registration, gets its service credential from the\nspace's auth provider, verifies the bearer signature and witness against the pinned authority's\nissuer keys, checks the serving snapshot against its embedded contract, and connects to the broker\nas that service. It binds the registered\nresponder before reporting ready. It succeeds, exit 0, only after the broker accepts the connection,\nthe responder subscriptions are flushed and the space accepts the daemon as ready. The renewal and ready request IDs are kept in\n`<COTAL_HOME>/daemon/<space segment>/service-store/`, mode `0600`, and reused after a restart.\n\nEvery other answer is a refusal and nothing is connected. Not yet approved is `LIFECYCLE_BLOCKED`\n(`APPROVAL_PENDING`). A refused registration is `PERMISSION_DENIED`, and so is a space that ended the\ndaemon's authority, with its code as the native code, such as `APPROVAL_REVOKED`; connecting again\ndoes not reconnect it. A space or broker that cannot be reached is `DEPENDENCY_UNAVAILABLE`, and\nconnecting again retries the same requests. Without a register request the answer is\n`TARGET_NOT_FOUND`.\n\nWhile the daemon runs, the connection renews itself and reconnects on its own. The access bearer\nlives at most 60 seconds, so the broker drops the connection when each one expires and the daemon\nreconnects with the next. While the broker connection is down, for that reason or any other, the\nanswer is `connecting`, not connected; it is connected again only after the broker is back and the\nspace accepts the daemon as ready again. This is not a persistent service: a restarted daemon starts\nwith no service connection and stays disconnected until you run `service-connect` again, and an\nended connection is remembered only until the daemon stops.\n`service-status` reads the daemon's current answer without contacting the space. `service-disconnect`\ndrains the responder and closes the connection, and stopping the daemon closes all of them.\n\nThe service answers its existing `machine-status` command with `null`. This is a liveness response;\nit contains no vitals, hostname or machine metadata. Its stock Describe answer is explicitly public:\nit identifies the endpoint and derived owner token and includes the protocol and command schemas.\nCallers still need broker-enforced invoke access to the selected instance and command. Connecting the\ndaemon grants no caller access. Owner approval, operator name binding and caller grants remain\nseparate actions. An authority that does not publish its issuer identity cannot support this\nserving connection; the daemon refuses rather than trusting a decoded token or using Manager\ncredentials. The registration contract is unchanged.\n\n## service\n\n```bash\ncotal service [--space <s>] [--json|--plain]\ncotal service show <name>\ncotal service add <name> --preset <preset> <preset-options>\ncotal service add <name> --file <definition.json>\ncotal service start <name> [--instance <id>|--new-instance] [--foreground|--dry-run]\ncotal service stop <name> [--instance <id>] [--with-agents] [--yes|--dry-run]\ncotal service restart <name> [--instance <id>] [--with-agents] [--yes|--dry-run]\ncotal service status <name> [--instance <id>]\ncotal service logs <name> [--instance <id>] [--tail <n>] [--follow]\ncotal service install <name> [--instance <id>] [--dry-run]\ncotal service uninstall <name> [--instance <id>] [--yes|--dry-run]\ncotal service remove <name> [--yes]\n```\n\n`svc` and `services` are aliases. Every form takes `--space`, `--json` and `--plain`.\n\n| Flag | Meaning |\n|---|---|\n| `-s, --space <s>` | The space whose services to use, resolved as in [Selecting a space](#selecting-a-space). A space that is prepared but not started can be named |\n| `--preset <name>` | add: save a declared preset. Its options become flags of `add` |\n| `-f, --file <definition.json>` | add: import a version-1 definition file |\n| `--instance <full-id>` | Select one instance by its full ID, not a prefix |\n| `--new-instance` | start: allocate another instance of a process definition |\n| `--foreground` | start: keep the process in this terminal |\n| `--with-agents` | stop, restart: request the manager's native agent-stop policy |\n| `--dry-run` | start, stop, restart: preview the effect. install, uninstall: inspect the unit |\n| `-y, --yes` | stop, restart, remove, uninstall: confirm in a script |\n| `--tail <1..1000>` | logs: how many records to read (default 100) |\n| `--follow` | logs: keep reading |\n\nA service is a saved definition in one space plus the instances started from it. Bare `service`\nlists the definitions, their instances and the installed presets. Nothing starts a dependency for\nyou: starting the manager does not start its broker. `--json` writes one envelope for every form,\nand `logs --follow --json` writes one record per line.\n\n### Adding a service\n\nThe presets built into the CLI are `auth-service`, `broker`, `delivery`, `exec-endpoint`,\n`feedback-intake` and `manager`. The `linear` and `web` presets come with the `@cotal-ai/linear`\nand `@cotal-ai/web` extensions, which `cotal ext add <package>` installs. Bare `service` lists the\npresets this machine has, and a name it does not have is refused as `DEPENDENCY_UNAVAILABLE`.\n\n`cotal service add <name> --preset <preset> --help` lists a preset's options. A required option is\nmarked `(required)`, and an option that takes a path shows `<dir>` or `<file>`. A missing required\noption is refused with each one named.\n\n`add` checks the options against the preset's schema, and checks that each declared path exists\nwith its required protection. It does not check that a root or credential suits the preset, such\nas a root's auth mode or which credential a file holds. Those are checked when the service starts.\nA refusal names the option or field at fault. Option values are not printed.\n\n`--file` imports a version-1 definition. This one runs a program and treats it as ready once its\nloopback health URL answers 200:\n\n```json\n{\n \"schemaVersion\": 1,\n \"space\": \"main\",\n \"launch\": { \"type\": \"process\", \"executable\": \"/usr/bin/node\", \"args\": [\"/srv/reviews/server.mjs\"], \"cwd\": \"/srv/reviews\" },\n \"readiness\": { \"type\": \"http\", \"url\": \"http://127.0.0.1:8080/health\", \"expectedStatus\": 200, \"timeoutMs\": 10000 },\n \"envRefs\": { \"REVIEWS_TOKEN\": \"REVIEWS_TOKEN\" },\n \"credentialRef\": { \"kind\": \"file\", \"path\": \"/srv/reviews/reviews.creds\" }\n}\n```\n\n| Field | Meaning |\n|---|---|\n| `schemaVersion` | `1` |\n| `space` | The selected space's name. Another name is refused |\n| `launch` | `{type: \"process\", executable, args, cwd}`: an executable on `PATH` or by path, its arguments, and an absolute working directory. Or `{type: \"adapter\", adapter, options}` for an installed adapter, which a preset writes for you |\n| `readiness` | `{type: \"process\", timeoutMs}`: ready once the process runs. `{type: \"http\", url, expectedStatus, timeoutMs}`: ready when a loopback `http://127.0.0.1` or `http://[::1]` URL answers that status. An adapter definition uses `{type: \"adapter\", timeoutMs}`. `timeoutMs` is 1 to 120000 |\n| `envRefs` | Optional. Child variable name to the name of a variable in the environment `start` runs in. `CLAUDECODE` and names starting `COTAL_` or `CLAUDE_CODE_` are refused |\n| `credentialRef` | Optional. `{kind: \"file\", path}`: an absolute, protected credential file. The child receives its path as `COTAL_CREDS`, never its contents |\n\nNo other field is accepted. The file is at most 256 KiB.\n\n### Running a service\n\n`start` records the instance before it launches anything and prints its full ID. A start that fails\nbefore its process exists records the instance as failed. Its refusal names the cause, such as a\nspace that is prepared but not started. `start` can then try again on the same instance, and\n`stop`, `remove` and `cotal down` find nothing to stop.\n\n`stop` and `restart` ask before they act in a terminal and need `--yes` in a script. Enter, Ctrl-C,\nCtrl-D or closed input at that question leaves the service unchanged and exits 0. Without an\ninstalled unit, a `restart` whose process is already gone stops nothing, so it does not ask. A\nstart or stop that finds nothing to do says so and reports `no-op`.\n\n`remove` forgets a definition once every instance is stopped and no unit is installed. Logs stay.\n\n`install` writes an owned systemd user unit for a saved direct-process definition with no\n`envRefs` or `credentialRef`, and enables it without starting it. It is Linux only. `uninstall`\nremoves that unit. A preset is not installed as a unit in this release, the manager included.\n\nThe sections [Local service definitions](#local-service-definitions),\n[Service presets](#service-presets) and\n[Owned process services](#owned-process-services)\ndescribe each preset and the process driver in detail.\n\n## reconcile-gate\n\n```bash\ncotal manager reconcile [--space <s>] [--server <url>] [--endpoint <e>] [--instance <id>] [--yes|--dry-run] [--json|--plain]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space the frozen gate lives in |\n| `--server <url>` | the local mesh | Broker URL |\n| `--endpoint <e>` | `manager` | Endpoint whose gate is frozen |\n| `--instance <id>` | this folder's persisted manager instance | Instance id |\n| `--yes` | ask and default to cancellation | Confirm this selected gate repair outside a terminal |\n| `--dry-run` | off | Resolve the local coordinate without minting or checking native guards |\n| `--json`, `--plain` | terminal report | Finite structured receipt or plain shared report |\n\n**When you need this.** A manager restart killed after deregistration begins but before the new\nincarnation finishes leaves the endpoint's issuance gate *frozen*, held by a\nprocess that no longer exists. The freeze is what stops two incarnations serving at once, which is\ncorrect. The successor manager now completes that dead registration itself on boot, including on\nthe remote user-auth path. A foreign remote manager blocked by this gate also asks the host to repair\nit before one registration retry. Both use the same guard this command uses: they act only when the freeze-holder is affirmatively gone under a complete\nCONNZ sweep (`gone` and `sweepComplete=true`). If that registration's spec write already committed,\nit finishes the same freeze at the committed registration revision. If the spec did not advance, it\nabort-reopens the gate at generation+1 with processEpoch unchanged and continues the normal takeover.\nLive, unknown, unestablishable, and\nwrong-op-kind still refuse; there is no TTL.\n\nUse this command when the automatic path cannot run: the delivery daemon is down, the repair targets a\nnon-manager endpoint, or you want to lift the freeze without starting a manager. It checks that the\nholder really is gone, then finishes the dead operation the same way as the\ninterrupted restart would have: revoke the old credentials, evict their holders with verification,\nand reopen the gate.\n\nThe command revokes the old credentials 16 at a time. It then verifies the holders' eviction in\nshared sweeps of up to 256 holders on the delivery daemon. Each sweep scans the broker a fixed number\nof times and kicks live connections 16 at a time, so holders that are already gone add almost\nnothing and live ones add one broker round trip per 16 connections. The daemon must serve the\n`evictPrincipals` verb; an older daemon refuses it and the gate stays frozen.\n\nEach sweep durably records the holders it verified before the next sweep starts. If a holder is not\nverified gone, the command leaves the gate frozen with those records kept. An interrupted sweep\nrecords nothing, and the sweeps before it stay recorded. A retry still repeats the freeze-holder\nliveness check, then skips only progress bound to the same registration operation, frozen-gate\nrevision, and holder set.\nThe output reports holders completed before this attempt, completed now, and still remaining. A new\nfreeze or changed holder set starts from zero. Cursor cleanup happens only after reopen; a retained\ncursor is harmless because its old gate revision cannot authorize a later freeze. The governance slot\nrelease after the reopen is cleanup in the same way: a slot it fails to release does not fail the\nrepair, because the reopened gate is already past the slot's generation.\n\n**It refuses far more often than it acts, on purpose**, and always says which check stopped it:\n\n| Refusal | What it means | What to do |\n|---|---|---|\n| `holder-alive` | The freeze-holder still has a live connection: a manager *is* running | Stop that process first. Reconciling would evict a live manager's credentials |\n| `holder-unknown` | The connection sweep could not prove the holder absent | Not safe to proceed: an unprovable holder is treated as a live one. Re-run once the broker answers completely |\n| `liveness-unestablishable` | The delivery daemon gave no verdict: it was unreachable, timed out, or refused | Act on the delivery lease line in the refusal (below). Silence is never read as death |\n| `not-frozen` / `no-gate` | The gate is open, or there is no gate at that coordinate | Nothing to repair: check `--endpoint` / `--instance` |\n| `wrong-op-kind` | Frozen under a takeover or retirement, not a registration | Out of scope for this command; it will not reinterpret another operation's intent |\n| `eviction-unverified` | The holder looked gone but eviction could not be verified | The gate is left frozen, unchanged. Investigate the broker before retrying |\n| `raced` | A newer manager moved the gate mid-repair | Re-run `cotal doctor` and look again |\n\nWhen the daemon gives no verdict, the refusal also reads the delivery lease (`lease.0`) and names\nwhat is blocking the rail:\n\n| Lease reading | What to do |\n|---|---|\n| absent | No daemon is running. Start it (`cotal up` runs it) and re-run |\n| unreadable | The daemon cannot be named, so do not assume none is running. Fix the lease read, then re-run |\n| held, not ready | That holder claimed the shard and has not bound its rails. Wait for it, or stop it so its lease lapses |\n| held, ready, no answer | The query may have gone to another daemon still subscribed to the rail, such as a stopped one whose lease lapsed. Re-run before stopping anything. If no run gets an answer, stop any other delivery daemon for the space, then stop or restart the holder |\n| changed hands | The holder took the shard after the query was sent, so it was never asked. Re-run before stopping anything |\n\nThe command reads the lease before it sends the query and again after the query fails. It names a\nholder as the blocker only when the same run of the same daemon held the lease both times, and two\nrows from a daemon too old to record its run never count as the same run. Even then a ready holder\nmay not have been asked: the rail is queue-grouped, so any daemon still subscribed to it can take\nthe query. A row whose times are not valid dates reads as unreadable.\n\nA daemon that answered and refused keeps its own reason, followed by the same lease line. The lease\nline names the holder, whether it is ready, the space account that holds the lease bucket, when that\nholder acquired the shard, and when the row was last written. A ready holder rewrites the row on\nevery renewal and keeps its acquisition time, which only a successful acquisition sets. A row\nwritten by a daemon that predates the acquisition time reports it as unknown. The lease reads never\nchange the outcome: the gate stays frozen and the command exits 2. A manager's boot self-heal uses\nthe same check and reports the same line.\n\n`raced` means the repair's own reopen lost its compare-and-set and wrote nothing. A reopen whose\noutcome is unknown, such as one whose acknowledgement was lost, fails with an `unavailable` error\nthat says it may have committed. Re-run the command: it refuses `not-frozen` if that reopen landed and\nfinishes the repair if it did not.\n\nThere is no `--force`, and no path that discards gate state: the only way this reopens a gate is by\nproving the holder is gone and then completing the operation properly.\n\n**What reopening the gate does for the endpoint's governance slot.** A registration takes the\nendpoint-wide governance slot before it publishes its spec, and holds it until its gate reopens. An\ninstance that died between those two points leaves the slot held with no registration behind it.\nThis command does not write that slot and never has; the registration path is its only writer. What\nthe reopen does is advance the holder's gate past the generation the slot is stamped with, which is\nwhat marks the slot abandoned. The next registration for that endpoint then reclaims it as part of\nits ordinary start. So the repair here is still one command followed by starting the manager, and\nthe slot needs no separate step.\n\n## endpoint deregister\n\n```bash\ncotal endpoint deregister <class> <full-instance-id> [--space <space>] [--server <url>] [--yes|--dry-run] [--json|--plain]\n```\n\nThis removes a departed instance's registration, not a process or an agent. Both the native\nclass and full instance ID are required; there is no default instance, prefix match or saved\nreference. The selected static space must have validated local signing authority. Open and\nuser-auth targets refuse instead of borrowing a static credential path.\n\nThe command resolves one root, account and broker policy, including its TLS requirement.\nA terminal reviews that full target and defaults to Cancel. Scripts need `--yes`.\n`--dry-run` reports the offline target and intended guard without contacting the broker,\nissuing an operation credential or deleting anything. It does not prove apply will be allowed.\n\nApply delegates to the existing native guard. An answering instance refuses. An unanswered\ninstance also refuses unless the broker affirmatively reports that instance rail empty.\nAn unavailable probe or an in-flight registration refuses. After that evidence, the native\nhelper checks governance and deletes status before spec, pinning both revisions. The issuance\ngate is not changed and no process is stopped; the same instance may register on a later start.\n\nThe JSON/plain receipt retains the full coordinate and acknowledged removed revisions.\nA registration that moves during deletion is not success: its spec was not removed, but an\nolder status may already have been removed. That uncertain outcome retains the target and is\nnot retried. Raw probe diagnostics and credential material are not copied into the receipt.\n\nThe old `deregister-instance` spelling is a hidden migration refusal. It starts no operation\nand names the literal replacement; its former default-to-this-folder instance is not retained.\n\n## runtimes\n\n```bash\ncotal runtimes [--json | --plain]\n```\n\nLists every agent runtime the manager can spawn through: the built-in `pty`, the official providers\n(`orca`, `tmux`, `cmux`, `herdr`), and any custom provider installed via `cotal ext add`. Each installed\nprovider is probed so you can see what is actually reachable on this machine before selecting it:\n\n```\nRUNTIME STATE PACKAGE\npty built in\norca installed \xB7 reachable @cotal-ai/orca\ntmux not installed @cotal-ai/tmux\ncmux not installed @cotal-ai/cmux\nherdr not installed @cotal-ai/herdr\nadd one that is not installed with `cotal ext add <package>`\n```\n\n`installed \xB7 reachable` / `unreachable` is the provider's own `available()` probe; `not installed`\nmeans it is a known runtime you can add with `cotal ext add` and the package shown. `--json` writes\none document whose `result.items` are `{name, state, package, reachable, error}`, with `state` one of\n`built-in`, `installed` or `not-installed` and `reachable` null when the provider was not asked. An installed provider\nwhose package fails to load is shown with its error and makes the read `partial` (exit 4). The command\nreads this machine only, so its `target` is null. Selecting an unknown or uninstalled runtime\nvia `up`/`spawn --runtime <name>` fails loud and, for a known one, points at the exact `cotal ext add`\npackage. There is no silent fallback to `pty`.\n\n## seats\n\n```bash\ncotal seats [--drain]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--drain` | off | Retire every seat whose agent has exited. A seat whose agent still runs is kept |\n\nThe pty runtime used to start a detached custodian process for every Linux seat. It now spawns\nin-process, but custodians that an earlier manager started keep running, and one whose agent has\nexited stays resident while a manager still holds its connection. This command lists the custody\nrecords under `COTAL_SEAT_ROOT` (default `~/.cotal/seats`), one line per seat:\n\n| State | Meaning |\n|---|---|\n| `live-child` | The agent process still runs. The seat is never signalled, and a manager can still adopt it |\n| `childless` | The agent has exited, or the record comes from an earlier boot. `--drain` retires the seat |\n| `drained` | `--drain` proved the custodian and the agent gone and removed the record |\n| `refused` | The record cannot be read, carries no start or boot identity, this host publishes no boot identity, or the reap could not prove the processes gone. The record stays on disk |\n\nA drain signals only a custodian whose recorded start identity still matches the live process, so\na reused pid is never touched. No process outlives a reboot, so a record from an earlier boot is\nreported childless and `--drain` removes it without signalling anything. A record with no start or\nboot identity is refused with or without `--drain`, and is never reported as running or exited.\nOn a host that publishes no boot identity (`/proc/sys/kernel/random/boot_id`) every record is\nrefused the same way, because no record can be tied to this boot.\nThat refusal and an unreadable record signal nothing. A refusal from the reap itself can come after the drain already\nsent `SIGKILL` to the custodian. Its detail names the pid or process group the reap could not prove\ngone, so check those processes before you retry. The command exits non-zero when any record is\nrefused. It is Linux-only and throws on other platforms.\n\n## send\n\n```bash\ncotal send -t <agent> \"<text>\" [--space <s>] [--server <url>] [--creds <path>]\ncotal send -c <channel> \"<text>\"\ncotal send -r <role> \"<text>\"\n```\n\nChoose one destination: `--to/-t` for a direct message, `--channel/-c` for a channel,\nor `--role/-r` for the role's native work queue. Choose one body source: a quoted positional\nargument, `--file/-f <path>`, or explicit `--stdin`. There are no implicit stdin reads. Files and\nstdin must contain nonempty UTF-8 text; their content is preserved and never printed in the receipt.\nNo destination, several destinations, several body sources or an empty message is refused.\nThe recipient grammar is checked before connector installation or target preparation. Channel\nand role names use the native subject grammar; a channel publication cannot name a wildcard.\nA direct target resolves by its full identity or an unambiguous name. A name collision refuses;\nno fuzzy match selects an agent for you.\n\nFor a channel post, repeat `--mention <agent>` to add explicit wake hints without changing the\ndestination. Direct and role sends have no native mention field and refuse that option.\nA direct send may use `--reply-to <message-id>` to identify the message it answers. The sender\nstill has no inbox and does not promise thread continuity.\n\nThe receipt names the destination and message identifier without repeating the body. Direct and\nrole sends include the broker's stored sequence. Storage does not confirm delivery, acceptance\nor completion. A direct send to an agent the roster shows offline also warns on stderr:\n`<agent> is offline. The message is stored and delivered when <agent> reconnects; a new agent spawned as <agent> does not receive it.`\n`--json` lists it as the `RECIPIENT_OFFLINE` warning. Use `--json/-j` for the same receipt as one versioned envelope, `--plain` for a text report, or\n`--verbose/-v` to show the actual sending principal. An unregistered channel is labeled; its native\nspace policy still controls publication and no registry metadata is created. Unreadable metadata\nstays not checked. An unconfirmed publish outcome is unknown and never retried automatically.\nThe command sends once and exits. It does not wait for an answer, retain an inbox\nor transfer replies to a later chat session.\n\nThe sending principal comes from the selected space's credentials or user login. Its display name\nis the OS login and host, not `COTAL_NAME`. A one-shot sender never registers as a participant.\nThe broker and native channel policy still decide which publications are authorized.\n\nThe transient endpoint never joins the roster and binds no inbox. With a static or open-mesh\ncredential it reads the roster and the channel registry once when it connects and binds no watch\nconsumer, so it leaves nothing on the broker when it exits. On a user-auth mesh it runs on your user\nbearer, which has no batched read of those buckets, so it watches them instead, and each watch's\nconsumer stays on the broker for five minutes after the send exits. A sustained send rate there can\nreach the broker's limit of 1000 consumers per stream, and the broker refuses further sends until\nthe old consumers expire. A channel send also reads that channel's policy, with one Direct Get on a\nstatic or open mesh and with one more scan consumer on a user-auth mesh.\n\nA recipient can still answer a\n`send --to` or `send --role` with `cotal_dm`, by the sender's name or by the id on the message it\nholds: the reply is stored under the sender's id in the space's DM history, which an operator's DM\nview such as the dashboard's Direct messages lens shows. The `cotal send` that asked has already\nexited, so the reply never reaches that shell.\n\n## chat\n\n```bash\ncotal chat [<channel>] [--name <n>] [--plain | --stdio] [--space <s>] [--server <url>]\n```\n\n`chat` opens one live session in the selected space, under an identity of its own. Each opening is\na new principal with its own inbox and presence, so two windows are two sessions, and leaving ends\nthe session. `--name` sets its display label; the default is your OS login and host with a short\nsuffix. The label carries no authority.\n\nThe mesh is resolved once, and its recorded auth mode picks the admission. No other mode is tried.\n\n| Mode | Admission | Lifetime |\n|---|---|---|\n| open | Routing identity only, with no credential | Until you leave |\n| static | The space's manager admits the session and mints its own credential ([Static chat sessions](control-surface.md#static-chat-sessions)) | 15 minutes, never renewed |\n| user | The auth service admits it under your own `cli` grant; sign in with `cotal login` first | Up to 8 hours, never extended |\n\nA named channel is the session's initial channel. On a static or user-auth mesh it is also the\nsession's whole read and post list, so a session opened without one can send and receive direct\nmessages only. A send, join or channel mode outside that list refuses with `PERMISSION_DENIED`\nbefore anything reaches the broker. On a session opened without a channel, the refusal says to\nleave and open a new session on the channel. Before the session is admitted, `chat` reads the channel registry on the resolved\nmesh the way `send` does, under the same one-shot identity, and says on stderr whether the channel\nis registered, unregistered, or could not be read. The space's native policy decides whether a send\nis allowed, and nothing creates registry metadata.\n\nThe terminal view needs a terminal for input and output; without one, `chat` refuses and names\n`--plain` and `--stdio`. `--plain` is the line flow, with a separate confirmation for each send.\n`--stdio` runs the NDJSON participant protocol for programs: requests on stdin, records on stdout,\ndiagnostics on stderr. Closing stdin ends the session. `--stdio` does not combine with `--plain`,\nand `chat` has no `--json`. Under `--no-input` only `--stdio` runs.\n\nA direct message to an agent the roster shows offline is stored, and the view adds a notice that\nthe agent receives it when it reconnects, as [`send`](#send) warns. `--stdio` carries that roster\nstatus as `recipientStatusAtSend` in the stored result.\n\nMessage times show in local time, with the date when it is not today, the same way `history`\nshows them.\n\n`/reply` answers the latest direct message or role task the session received. It addresses the\nsender's principal and quotes the message id, with no name lookup. A sender shows by display name,\nand with its principal when another principal in the session carries the same name, ignoring case.\n`--plain` cannot relabel a line it already printed, so it prints one line naming those principals\nwhen a name becomes shared. A typed `@<name>` must name one principal among the present peers and\nthe senders the session heard from. A name that more than one of them carries refuses and lists\nthem, so type `@<owner>.<actor>` instead. A target that parses as `<owner>.<actor>` goes to that\nprincipal whether or not it is present or was heard from, and is never matched against display\nnames. A peer whose display name parses as a principal therefore cannot be reached by that name;\n`/reply` and its full principal still reach it. Typing the session's own principal refuses with\n`USAGE_INVALID`. A `--stdio` `dm` request resolves its `to` the same way.\n\nEvery admission presents the session as an endpoint, so `--kind endpoint` is accepted and changes\nnothing. `--kind agent` refuses with exit 1 because a chat session never presents as an agent, and\nso does any other kind.\n\n`--role`, `--link`, `--token`, `--creds` and `--lifecycle-uid` are declared but no admission\nsupports them yet, so each refuses with exit 2. A role would put the session on that role's work\nqueue, and the view has no way yet to answer the sender of a task. The static and user-auth\nadmissions take no role.\n\nA session that ends by leaving, by end of input or at its lifetime exits 0 when its connection\nclose was confirmed and it was retired, or was an open-mesh session with nothing to retire. It exits\n4 when its connection close was confirmed and its retirement was requested but not confirmed. It\nexits 3 when the retirement outcome is unknown, its connection close was not confirmed, or no close\nwas observed. In the terminal view Ctrl-C and Ctrl-D leave. With `--plain` or `--stdio`, Ctrl-C ends\ninput and the session closes, then exits 130. A terminate signal exits 143 and a hangup 129. A\nsignal that arrives before the view starts is held: no session is opened if admission has not\nbegun, and a session admitted meanwhile is closed at once. The exit status is still the signal's.\nWhen the reader of `--plain` or `--stdio` output closes it, as `| head` does, input ends the same\nway and the session closes, then exits with its close status: 0, 3 or 4.\n\nBefore a session is admitted, an error prints on stderr only, since `chat` has no `--json`. An\nerror that ends `chat` exits 1 for `USAGE_INVALID` or `SCHEMA_INVALID`, 2 for any other refusal, 3\nwhen the outcome is unknown and 5 when it failed.\n\n### The --stdio protocol\n\nstdin carries requests and stdout carries records, one JSON object per line, in UTF-8 with LF line\nends. A CR before the LF is dropped, a blank line is ignored, and a last line with no LF still\ncounts when input ends. A line longer than 1048576 UTF-16 code units is discarded up to its next LF\nand reported once with an `error` record. Input is decoded as UTF-8 with replacement, so bytes\nthat are not valid UTF-8 become U+FFFD. In ordinary string content the line can still parse: a\n`text` value is sent with U+FFFD in their place, and a field that accepts only some characters,\nsuch as `id`, refuses. In an escape sequence such as `\\` or `\\u` and its four digits, or outside a\nstring, they make that line's JSON parse refuse. stdout carries records and\nnothing else. The session's diagnostics go to stderr, prefixed `cotal chat: `, and never repeat a\nmessage body.\n\nEvery request and record carries `\"schemaVersion\":1`. A request names its `type` and an `id` you\nchoose, 1 to 128 printable ASCII characters, and its `result` record repeats that id. A request that\nreuses the id of one still in flight is refused with `USAGE_INVALID`, in a result under that same\nid. Requests run one at a time in arrival order, so results come back in that order. `message`,\n`presence` and `gap` records can arrive between results, so match each result by its id. A missing\nfield, a field the type does not take or a malformed value refuses with `USAGE_INVALID` before the\nsession sees the request. A channel must be a valid channel name with no `*` or `>`, and a role a\nvalid role name.\n\n| `type` | Fields | Result |\n|---|---|---|\n| `send` | `channel`, `text`, optional `mentions` (names of 1 to 128 characters) | `stored`: `messageId`, `channel`, `registered` |\n| `dm` | `to` (a display name or `<owner>.<actor>`, 1 to 512 characters), `text`, optional `replyTo` (a message id, 1 to 512 characters) | `stored`: `messageId`, `seq`, `duplicate`, `recipient` (`id`, `name`) |\n| `anycast` | `role`, `text` | `stored`: `messageId`, `seq`, `duplicate` |\n| `status` | `status` (`idle`, `working` or `waiting`), optional `activity` | `ok` |\n| `attention` | `mode` (`open`, `dnd` or `focus`) | `ok` |\n| `channelMode` | `channel`, `mode` (`normal`, `quiet` or `muted`) | `ok` |\n| `join` | `channel` | `ok`: `joined`, `backfilled`, `durable`, optional `reason` |\n| `leave` | `channel` | `ok`: `left` |\n| `inbox` | optional `peek` (default `false`) | `ok`: `peek`, `messages` |\n| `roster` | none | `ok`: `rows`, `view` |\n| `channelInfo` | `channel` | `ok`: `channel`, `registered`, `replay`, `readable`, `postable`, optional `description` and `instructions` |\n| `connection` | none | `ok`: `state`, `identity`, `bufferedInbox`, `joinedChannels`, `attention`, `channelModes` |\n| `invoke` | `endpoint` (1 to 200 characters), `command` (1 to 128), optional `args` (an object) and `timeoutMs` (an integer from 1 to 120000, default 30000) | `ok`: the endpoint's data, or `null` when it returned none |\n\n`identity` in the `connection` result names the session, and no other record names its space. It\nhas `space`, `mode` (`open`, `static` or `user`), `principal` (`<owner>.<actor>`), `lifecycleUid`,\n`label`, the `allowSubscribe` and `allowPublish` ceilings, and `expiresAt` in epoch milliseconds\nwhen the session has a set end. A roster's `view` is `current`, `unpopulated` or `stale`, and its\n`rows` are complete only when it is `current`. A row has `id`, `name`, `kind` and `status`, and\n`role`, `activity` and `sessionKind` when they are set.\n\nA `result` record has `type`, `id` and `outcome`. With `ok` or `stored` it carries `result`, which\nis `null` when the request returns nothing. With `refused`, `unknown` or `failed` it carries `error`\nwith `code`, `message` and, when the native layer gave one, `cause` with its own `code` and\n`detail`. `refused` means nothing happened. `unknown` (`OPERATION_UNKNOWN`) means the effect may\nhave happened. The session never retries it, and it reports an unexpected failure of its own the\nsame way. `failed` (`OPERATION_FAILED`) is a step that did not complete and had no effect. `stored`\nmeans the broker's stream accepted the message. It does not say anyone read it.\n\nThe session also writes these records on its own:\n\n| `type` | Fields | When |\n|---|---|---|\n| `message` | `message`: `id`, `ts` (epoch milliseconds), `kind` (`channel`, `dm` or `anycast`), `from` (`id`, `name`, optional `role`), `channel` or `service`, `text`, optional `replyTo`, `mentionsMe`, `historical`, optional `deliveryCount` | A message arrived |\n| `presence` | `change` (`joined`, `updated` or `left`), `row` (a roster row) | A roster row changed, this session's own row included |\n| `gap` | `scope` (`live` or `inbox`), `reason` | Messages may have been missed |\n| `error` | `id` (`null`), `error` | A line was refused before an id could be read from it, or was too long |\n| `end` | `reason`, `close` | The session ended. Nothing follows it |\n\nA message's `text` is untrusted. JSON escapes its control characters, so a body can never start\nanother record. `channel` is set on a channel message and `service` on an anycast. `mentionsMe`\nmeans the message's mention list holds this session's label, ignoring case. The sender writes that\nlist, so treat it as a hint. `deliveryCount` appears only on a durable delivery and is 1 the first\ntime. Above 1, an earlier copy went unacknowledged and may already have been shown.\n\nA message is acknowledged once its record is handed to stdout. A DM or anycast is written when it\narrives. A channel message is held in the inbox instead when its channel is quiet, when attention is\n`focus`, or under `dnd` unless it mentions the session, and join backfill is held too. A muted\nchannel's messages are dropped. `inbox` with `peek` lists the held messages and leaves them held.\nWithout `peek` it returns them, and they are acknowledged once its result is handed to stdout. A\nmessage still unacknowledged when the session closes is left unacknowledged and does not come back\nto this session.\n\nA `live` gap means the connection dropped or is reconnecting, so live channel messages may be\nmissed until it returns. Its reason defaults to\n`connection <state>: live channel messages may be missed until it returns`. An `inbox` gap means\nthe held buffer was full: channel messages were dropped and direct messages were left for the broker\nto redeliver. When a write waits for stdout to drain, or more than 4 MiB of records are waiting, the\nsession stops reading stdin until stdout drains.\n\n`end` is the last record. Its `reason` is `input closed` when stdin ended or a signal ended input,\nand `session ended` when the session ended by itself, for example at a static session's expiry.\nWhen stdin ends, the requests already read finish first. `close` has `retirement` (`retired`,\n`requested`, `unknown` or `not-applicable`), `connectionClosed` (`confirmed` or `not-confirmed`) and\n`message`. A static session's `close` can carry `credentialValidUntil` in epoch milliseconds: the\nbroker accepts its credential until then unless its eviction was verified. Once the session has\nended by itself and before the `end` record, a request refuses with `LIFECYCLE_BLOCKED` and\n`the session is ending; no new requests are accepted`. When the reader closes stdout, stderr says\n`stdout closed by the reader; ending the session`, and no later record is written, `end` included.\n\n## channel\n\n```bash\ncotal channel\ncotal channel set <name> [--replay | --no-replay] [--window <duration>] [--delivery <live|durable>] [--desc <text>] [--instructions <text>]\ncotal channel defaults [--replay | --no-replay] [--window <duration>] [--delivery <live|durable>]\ncotal channel show <name>\ncotal channel members <name> [--follow] [--json | --plain]\ncotal channel remove <name> [--purge] [--yes]\n```\n\n`channel` has the shorthand `ch`; `remove` also accepts `rm`. The inventory shows configured\nmetadata, not every channel with traffic. `show` includes inherited replay and delivery settings.\n`members` reads the broker-derived membership feed. Its timestamp is the last successful feed\npoll, not proof of current activity or access. With `--follow --json`, it emits membership NDJSON\nrecords. A missing feed remains unknown rather than an empty confirmed census.\n\nRemoval keeps retained history unless `--purge` is present. The confirmation names the channel,\nspace and history scope. Scripts pass `--yes`. Neither removal nor replay settings revoke grants\nor stop participants; future posts can create retained history again.\n\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Target mesh |\n| `--replay` / `--no-replay` | none | `set`/`defaults`: replay history to new joiners, or not |\n| `--window <duration>` | none | `set`/`defaults`: replay window, for example `24h` |\n| `--delivery <live|durable>` | none | `set`/`defaults`: choose the live-only or durable-backstop delivery class; history retention is separate. Shell completion offers both values |\n| `--desc <text>` | none | `set`: channel description |\n| `--instructions <s>` | none | `set`: instructions shown to joiners |\n\nInspects and edits the channel registry: replay and delivery policy, description, and joiner instructions. `live` has no per-member backstop; it does not disable ACL-bounded retained history. Changing a channel's delivery class does not settle already pending durable copies. `--delivery durable` requires a registered static-auth or user-auth space; an open or unregistered target has no established delivery-service support. A recorded auth mode does not prove that daemon is currently healthy. ACL\nsemantics (who may read or post) are set at mint / provision time, not here; see\n[Channels and permissions](channels-and-permissions.md). On a user-auth mesh, `list` rides your\nown login as is; `set` and `defaults` edit the registry over a short-lived\nchannel-writer view, which needs ledger scope `admin` ([Identity & auth](identity-and-auth.md)).\nOn a remote user-auth mesh that view is served by the public exchange; space-history `purger`\nand the read-only admin view are not.\n\nWith a static or open-mesh credential, `list` reads the registry with one Direct Get and leaves no\nconsumer on the broker. On a user-auth mesh your login has no Direct Get row on the registry, so\n`list` scans it with a consumer that stays on the broker for five minutes after the command exits.\n\n\n## history\n\n```bash\ncotal history [<channel>] [--dms] [--since <time|seq>] [--limit <n>] [--from <name>] [--grep <text>] [--follow] [--json | --plain]\ncotal history clear [--dms] [--yes | --dry-run] [--json | --plain] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Target mesh |\n| `--dms` | off | Also read DM history, which needs a DM read grant. With `clear`, also clear DM history |\n| `--since <time\\|seq>` | none | Only messages after an ISO 8601 time, an age such as `30m`, `2h` or `1d`, or a chat stream sequence |\n| `--limit <n>` | 50 | How many of the newest retained messages to read, 1 to 1000 |\n| `--from <name>` | none | Keep messages from this sender name or id |\n| `--grep <text>` | none | Keep messages whose text contains this, ignoring case |\n| `--follow` | off | Keep listening and print each new message |\n| `--json` / `-j` | off | One JSON document, or with `--follow` one JSON record per line |\n| `--plain` | off | The same report without color or marks |\n| `--yes` / `-y` | off | `clear`: confirm without prompting. Scripts and `--json` require it |\n| `--dry-run` | off | `clear`: show the selected target and streams without connecting, and purge nothing |\n\nReads the newest retained messages in the selected space, newest last: one channel, or every\nchannel the identity may read. `--limit` bounds the read, and `--since`, `--from` and `--grep` then\nfilter that window on this machine. They do not search the whole stream. `result.window` says how\nmany messages were read, whether older retained messages exist that the window did not reach, and\nwhether the window goes back to `--since`. An age outside the supported date range or a sequence\nthat cannot be represented without rounding is a `USAGE_INVALID` refusal before connecting.\nThe report shows each message's local time, with its date when it is not today; `result.items[].ts`\nis the ISO time.\n\nRetained history is not every message sent. The chat stream keeps a fixed number of messages per\nsender on each channel, so a busy sender's oldest messages are discarded while other senders'\nmessages from the same time remain. `result.retention` names the limit and each sender at it on the\nchannels read, and the report prints a `gap` line for each. A gap is a fact about the stream, not a\npartial read. `result.replay` gives each channel's replay policy for new members, read from the\nchannel registry. DM gaps are not reported.\n\nA channel the identity may not read is refused with `PERMISSION_DENIED` (exit 2), never shown as\nempty. On a user-auth mesh each channel read uses the connection's broker-enforced channel grant.\nRevocation follows the effective credential and connection retirement, as it does for live channel\nreads. Concurrent commands sharing one identity wait for the broker to release its transient history\nreader; no command deletes another reader. Each channel has a ten-second read budget, with a\nthirty-second budget for the whole channel walk. A busy or cut-short read fails with `OPERATION_FAILED` (exit 5) rather than showing\nan empty conversation. Reading every channel leaves out channels the grant does not cover and names\nthem in a `CHANNELS_DENIED` warning. DM history and `--since` by sequence are not available to a\nuser-auth identity. A part that could not be read is a warning and a `partial` run (exit 4): the\nretention, a replay policy, the channel list when a channel is named, or one channel of several.\n\n`--follow` prints the window and then each new message until interrupted. With `--json` each line is\none record: `{\"schemaVersion\":1,\"stream\":\"history\",\"type\":<type>,\"target\":\u2026,\"receivedAt\":\u2026,\"data\":\u2026}`.\nThe first record is a `snapshot` holding the window, then one `event` per message. A `gap` record\nsays the connection was re-established and messages sent while it was down are not shown. An\n`error` record names a failure on the live feed, and an `end` record closes the stream. The exit\ncode is the snapshot's.\n\n`history clear` purges retained history from every channel in the selected space; `--dms` extends\nit to direct-message history. It refuses extra channel names, read filters and `--follow` before\npurging. These inputs cannot narrow a clear operation. It is the same operation as [`clean history`](#clean). On a user-auth mesh the purge rides a short-lived\npurger view over your login, which needs ledger scope `admin` ([Identity & auth](identity-and-auth.md)).\n\n## console\n\n```bash\ncotal console [--plain] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Space to watch |\n| `--plain` | off | Line stream instead of the TUI |\n\nA live protocol view for a space: a lazygit-style TUI, or a plain line stream on `--plain`. On a\nuser-auth mesh it rides the read-only admin view over your login, which needs ledger scope\n`admin`. Inside the TUI, operator control (`D` kill, `:spawn`, `:status`, `:purge`) rides the\nsame per-action instrument path as `cotal stop` and `cotal ps`, never the observer; a raw\n`--creds` file cannot drive it. `a` (or `:attach <agent>`) runs\n[`cotal attach`](#managed-seats) in place and returns to the console on detach. See\n[Watch a mesh](watch-a-mesh.md).\n\n<a id=\"web\"></a>\n\n## Web dashboard\n\nThe dashboard is an owned service preset from `@cotal-ai/web`, not a top-level `web` command.\nIt is not started by `up`. An absent extension requires the explicit `cotal ext add @cotal-ai/web`;\nstarting a service never installs it or creates a caller credential.\n\n```bash\ncotal service add web --preset web --space main --root /absolute/registered/root --creds /absolute/private/dashboard.creds --read-only\ncotal service start web --space main\ncotal service status web --space main\ncotal service logs web --space main\ncotal service stop web --space main --yes\n```\n\nThis route requires a registered static-auth target, an existing protected credential file\nand explicit read-only intent. `--host` chooses a concrete bind/browser address, default\n`127.0.0.1`; wildcard binds refuse. `--port` is 1 to 65535, default 7799. Broker coordinates and\nrequired TLS come from the registered target. Broker TLS does not encrypt the dashboard's HTTP\nlistener. The file supplies the only connection authority.\nNo cached human login, managed-seat identity, operator signer or channel-purger credential is\nsubstituted. User-auth/open targets refuse, because no supported caller handoff is connected for them.\nThe page marks this service read-only and hides channel delete; the server also refuses it before\nreading a delete body or opening a mutation connection. The broker still enforces the file's scope.\n\nThe real child exclusively claims native PID custody. After the HTTP listener and signal handlers\nexist, it writes a create-only owner-private session record for the full instance and launch, then\na private process-initialization phase. This is not a fresh authenticated endpoint-health reply.\nLogs print only the session file's reference, never its bootstrap URL or readiness credential. A\nbackground `service start` ends with the `service logs` command for the new instance, which is where\nthat reference appears. An endpoint error or warning in that log names only the error's class, its\nname and code, never its message. Open the private file locally and use its single-use launch link in the\nintended browser. There is no automatic browser action. The existing cookie and origin gates remain in use. Confirmed close\nremoves only this child's unchanged session/PID records; uncertainty retains custody.\n\nForeground service start uses the shared terminal lifetime. OS installation and full native caller\nparity are not supported. An old cached `command:web` descriptor is hidden and refuses with the matching\nextension-update remedy before package loading or repair. It is not resolved by removing the\ndashboard, starting a second process or silently reinstalling it.\n\n## linear\n\n```bash\ncotal ext add @cotal-ai/linear\ncotal linear account add <name> --mode <write|readonly> (--token-stdin | --token-file <path>)\ncotal linear account login <name> --mode <write|readonly>\ncotal linear account show <name>\ncotal linear inventory <account> [--json|--plain]\ncotal linear call <account> <tool> [--args '<json>'] [--inventory <digest>] [--timeout <ms>]\ncotal linear resource <account> <uri> [--inventory <digest>] [--timeout <ms>]\ncotal linear prompt <account> <name> [--args '<json>'] [--inventory <digest>] [--timeout <ms>]\ncotal linear serve <account> --endpoint <reverse-dns-name> [--space <s>] [--server <url>]\ncotal linear caller <name> --endpoint <reverse-dns-name> --out <path> [--channels <a,b>] [--expires-in <s>] [--json|--plain]\n```\n\nServes one Linear account as a registered Cotal endpoint that agents call with `cotal_describe` and\n`cotal_invoke`, and gives the operator a direct client for the same server. It only talks to `https://mcp.linear.app/mcp`\n(mode `write`) or `https://mcp.linear.app/mcp/readonly` (mode `readonly`); there is no URL or header\noption. Use a separate account per Linear workspace, and prefer a `readonly` account with a\nrestricted read key wherever writes are not needed.\n\nAn account's credential is sent as `Authorization: Bearer` and is never taken from argv. `account add`\ntakes a Linear API key: `--token-stdin` stores it under the cotal home as a 0600 file, and\n`--token-file` points at an existing file that must not be readable by other users. The file is read\nat each use, so rotating it needs no restart. `account login` runs Linear's OAuth flow instead: it\nregisters a client, prints the authorize URL, and waits on a loopback redirect. It asks for scope\n`read` in `readonly` mode and `read write` in `write` mode, keeps the tokens in a 0600 file, and\nrefreshes them before a request is sent. OAuth requests only go to `https://mcp.linear.app`.\nRedirects are refused so the credential is never forwarded.\n\n`inventory` reads the server's capabilities and every page of its tools, plus resources, resource\ntemplates and prompts when the server advertises them. Names and schemas are printed as the server\nsent them. The digest covers the whole inventory; pass it to `--inventory` so a call is refused\nbefore dispatch if the inventory changed. Capabilities this command does not represent, such as\nresource subscriptions or logging, are listed under `unsupported`.\n\nBare `linear` and `linear account` show their scoped operations without opening an account or\ncontacting the upstream. Account add/show, inventory and the native call/resource/prompt forms\nsupport shared rich/plain reports and version-1 JSON envelopes. Scoped help and completion use\nthe same operation/flag declarations as validation. Extra operands and wrong-mode flags refuse\nbefore account access. Positional values are bounded to 8192 characters without terminal controls.\nPiped tokens are bounded to 1 MiB of strict UTF-8; their contents and argument parse errors are\nnot echoed. Account reports describe recorded configuration or a native save acknowledgement,\nnot current access to Linear. An account name with no record refuses with `TARGET_NOT_FOUND` and\nthe `account add` route. A record that exists but cannot be read refuses with\n`DEPENDENCY_UNAVAILABLE`.\n\n`inventory --json` retains the full native inventory inside the result envelope. Human reports\nshow at most 60 whole tool rows. Read-only hints remain upstream declarations, not enforcement or\nwork progress. `call`, `resource` and `prompt` retain the complete native reply under\n`result.reply` with `--json`; rich/plain output shows at most 60 whole result lines. The bearer\nis scrubbed by the native client; other upstream content is not claimed to be secret-redacted.\n\nA tool result with `isError: true` remains an MCP result (exit 0), with a visible warning. This does\nnot assert that the requested work succeeded. A native refusal with outcome `not-executed` returns\nexit 2; that includes a deadline that expires while queued, discovering or connecting. An unknown\nprotocol/transport outcome returns exit 3 without replay, including for read-only requests. The\nnative outcome and reason remain in the result. Errors carry a status or class, not HTTP response\nbodies. An interrupted account save or OAuth flow can return unknown because private state or an\nexternal authorization may already have changed.\n\nOAuth login keeps the native human authorization flow, so it offers plain output but not JSON\nand refuses when input is disabled. The existing foreground `serve` route keeps its native authority and human output; it does not\naccept finite-output flags. The existing `caller` route\nkeeps its native static issuer but supports shared JSON/plain private-file receipts. Its output\nmust be outside managed state in an existing protected physical parent. An existing destination\nrefuses before provisioning. Publication is create-only under the cooperative output lock, and\nuncertain provisioning/publication is not retried. It prints neither credentials nor environment-\nexport launch recipes. The preferred supported standalone path is now `auth mint --endpoint`\nafter a live digest-verified surface is available. No caller or managed launch is inferred from an\naccount, inventory or reply.\n\n### Serving the endpoint\n\n`serve` registers the account as an endpoint named by `--endpoint` and serves it until Ctrl-C. The\nname must be a reverse-DNS name in a namespace you own, such as `com.example.linear`; it is\nauthorized for this mesh's local owner only because you configured it. `serve` reads the inventory\nfirst, so an account that cannot be reached is never registered. It then walks the same path every\nregistered endpoint does: it publishes the contract to the contract store, opens the issuance gate,\nregisters a fresh instance, writes its ready status, and mints a scoped serve credential through\nthe gate. Every step runs on short-lived executor credentials scoped to this one endpoint instance.\nThe space signer stays in the `serve` process for renewal at 75% of the credential's life and is\nnever written out, printed, or handed to the serving connection. Ctrl-C stops serving, closes the\nLinear session and removes the instance's service record, each within 10 seconds. A start that\nfails after registration removes the record before it exits and says whether that removal\ncompleted; a registration that fails part-way rolls its own record back and reports when it could not.\n\nThe endpoint serves five fixed commands: `inventory`, `call-tool`, `read-resource`, `get-prompt` and\n`complete`. Each needs the one `linear.mcp` capability. That capability is permission to call the\nendpoint; which tools a call can reach is decided by the Linear account and its mode. `inventory`\nreplies in pages that fit a broker message: the first page carries the server, its capabilities,\nits instructions and per-section counts, and every page carries entries verbatim with an opaque\ncursor bound to the inventory digest. An entry is never cut: discovery refuses an inventory whose\nfirst-page head (server, instructions, capabilities) or any entry prints over 48 KiB the way an\nagent tool prints it (indented JSON, which grows with nesting), naming the entry, so the first page\nand every entry it accepts fit one reply and the tool an agent reads it with. A cursor or `inventoryDigest` from an older inventory is refused before anything is sent to\nLinear. `call-tool`, `read-resource`, `get-prompt` and `complete` require the digest the caller read.\n\n### Callers\n\n`caller` provisions an isolated caller on a static-auth mesh: an `agent` credential with its own\nmailboxes and request rows for only the five Linear commands on that endpoint, plus the channels\nin `--channels`. It carries no spawn, run, admin or provisioner capability. The credential is\nwritten 0600 to a create-only `--out` outside managed state and never printed. Its physical parent\nmust already exist and be protected. The shared receipt contains the native identity, lifecycle,\nexpiry and acknowledged footprint, not credential bytes. An existing output refuses before\nprovisioning. A failure after native issuance starts stays unknown with confirmed identity/file\nreceipts retained, and is not retried. The command does not print environment-export launch recipes.\n\nThis remains the native standalone caller exporter, not a connector setup or generic caller bundle.\nA supported bootstrap must carry its actual credential and minted lifecycle together; a file alone\nis not a native caller identity. No seat is started, and managed static `cotal spawn` agents still\nrefuse endpoint capabilities. `auth mint --endpoint` now covers supported untargeted synchronous\nsurfaces through live native resolution. Targeted/journaled/action issuance and caller/launch\nadapters are not supported.\n\nA per-user-auth mesh is refused by both `serve` and `caller`: there, endpoint and caller credentials\ncome from the remote auth service, which this package does not support yet. Nothing falls back to a\nlocal signer. An open mesh is refused too, because it has no credentials to scope.\n\nNot represented yet, and listed under `unsupported` when the server advertises them: resource\nsubscriptions, logging, progress notifications and experimental capabilities. The server cannot\ncall back for sampling, elicitation or roots. Every call acts as the account's one Linear identity.\n\n## pending\n\n```bash\ncotal pending <agent> [--durable <exact-dm-durable>] [--limit <1..100>] [--space <s>] [--json|--plain]\n```\n\nThe finite read reports the selected DM durable's native pending count, ack\nfloor, delivered sequence, frontier, retention limits and up to 20 candidate\nIDs by default. JSON and plain output use the common one-document result. A\nname must match one observed roster entry; an exact principal ID wins over a\ndisplay-name match, and an ambiguous display name refuses. `--durable` reads a\nprevious lifecycle by its complete durable name without substituting a\nsame-named current agent. An absent or unreadable durable is not `pending 0`.\nThe caller needs the native admin read authority; user-auth has no equivalent\nview here and refuses without a static or bare fallback. A failed read does\nnot prune a locally saved mesh registration.\n\nCandidates are read without acknowledging DMs. The transient read consumer is\ndeleted after the bounded pass; an unconfirmed cleanup or candidate read is\nreported as partial. Candidate IDs are not evidence of delivery, a missing\nmessage or complete history. Recipient contents are not printed. The retained\n`deliver pending` spelling keeps its earlier output shape.\n\n## deliver\n\n```bash\ncotal deliver [--space <s>] [--server <url>] [--tls] [--creds <file>] [--root <dir>] [--shard <n>] [--shards <n>] [--dev-mint]\ncotal deliver pending <name> [--limit <n>] [--durable <name>] [--json]\n```\n\nWith no positional, `cotal deliver` runs the delivery daemon (see\n[the delivery daemon](delivery-daemon.md)). `deliver pending <name>` never starts the daemon: it\nis an operator-only read over one recipient's DM durable, for the moment after a send when the\nquestion is \"what does the broker actually hold for them.\" It resolves `<name>` against a short\npresence watch (an `offline` card still counts, since the recipient may be dead, that is what\nthe verb exists to inspect); when neither a card nor the durable can be found, it prints\n`\u2717 not-found: no agent \"<name>\" and no DM durable for it in space <s>` and exits non-zero, never\n`pending 0`. On a match it prints the durable name and one fact per line: `pending`,\n`ack-pending`, `delivered`, `ack-floor`, `created`, `frontier`, and the stream's `max_age` /\n`max_msgs_per_subject` / `discard` limits (`--json` prints the same facts as one object), followed\nby a bounded, unacked read of up to `--limit` (default 20) recent candidate message ids under the\nheading `recent candidate ids (from the ack floor; not proof of a hole)`, a list of what is\nthere, not proof that nothing was lost.\n\nThe verb needs the `admin` credential profile: it runs through the same static-mesh route as\n`cotal auth mint --profile admin`, and refuses a user-mode mesh, naming the retired static credential,\nbecause there is no user-mode inspection authority yet. Pass `--creds <file>` for an off-registry\nadmin credential. A same-name respawn never inherits a predecessor's held DMs (the durable is\nlifecycle-keyed); an old lifecycle's durable is reachable only by the name a live read printed\n(the `<durable>` line on the first line of this verb's output). Pass that name with `--durable\n<name>` to read it directly once the lifecycle's card is gone from the roster. This skips the\npresence watch on `<name>` entirely, so `<name>` is required but only echoed in error text.\n\n## auth mint\n\n```bash\ncotal auth mint <name> [--profile agent|observer|admin] [--out <private-file>] [--json|--plain]\ncotal auth mint <name> --provision [--role <role>] [--space <space>] [--server <url>]\ncotal auth mint <name> [--expires-in <seconds> | --expires-at <unix-seconds>]\ncotal auth mint <name> --identity <private-creds> --out <private-file>\ncotal auth mint <name> --endpoint <class> --out <private-file> [--channels <a,b>] [--expires-in <seconds>]\n```\n\nThe fixed-profile forms use the existing native static credential profiles. No generic host\nprofile is exposed.\nOpen and per-user-auth spaces refuse; a cached login does not replace signing authority. The\nresolved root supplies the persona ACL, signer and default storage together. A root that cannot be\nresolved refuses with its next step, such as `cotal up`. Conflicting current-folder and\nselected-root accounts refuse before issuance.\n\n`agent` is the default profile. Its channel ACLs and role come from the selected root's persona\nwhen present; explicit `--allow-subscribe`, `--allow-publish` and `--role` override them. An empty\nACL flag means no explicit entries. The native read-ACL resolver still enforces boot membership.\nObserver and admin have fixed native scope and reject agent ACL, role and provisioning flags\nrather than implying those inputs narrowed anything.\n\nThe default credential is created through its canonical secret-store kind. An existing label is\nnot overwritten. Use another label or an explicit private output. Explicit exports stay outside\nmanaged `.cotal` and workstation state. A pre-existing explicit output must be an owned private\nnative credential matching the nkey deliberately supplied through `--identity`; unrelated files\nand identities refuse. New outputs use create-only publication. The output's directories follow\nthe [`space create`](#selecting-a-space) path rule, which also accepts a root-owned sticky directory\nsuch as `/tmp`, and the file is owner-only. Unsupported platforms refuse rather than weakening it.\n\n`--identity` reuses the native nkey. The out-of-band agent mint reports its newly allocated\nlifecycle UID, with a warning that it did not reuse the old inbox/delivery footprint. Use that\nreported UID for the resulting client; do not infer it from the label or assume the old UID was\npreserved. No private nkey or JWT appears in a receipt.\n\nWithout `--provision`, fixed-profile minting writes credentials only. `--provision` uses one native provisioner\nconnection to pre-create that lifecycle's bind-only DM/delivery and role footprint. It performs\nthe authenticated preflight against the same recorded broker and TLS requirement, without\npruning a registration. An acknowledgement proves neither a joined agent nor task completion.\nA partial or unknown result retains the allocated public identity and lifecycle for diagnosis\nand does not retry the mint.\n\nLifetime inputs use native seconds. The receipt reports the resulting JWT expiry, rather than\npromising a default lifetime. `--expires-in` and `--expires-at` are mutually exclusive. JSON is\none finite document; plain output uses the same public receipt. Signing material is exported by\n[auth signer](#account-signer-export), not a signer flag on mint. The old top-level `mint` spelling\nis refused with the literal namespace replacement.\n\n### Endpoint caller credentials\n\n`--endpoint` is a separate closed mode, not an ACL override on an observer or admin profile. It\nrequires a private `--out` in an existing protected physical parent outside managed state. Only\n`--channels`, `--expires-in`, space/server and presentation flags combine with it. An empty or\nomitted channel list grants no explicit read/post channels. Supplied channels deliberately enter\nboth ACLs, as in the native standalone Linear caller form. No persona ACL or role is inferred.\n\nThe same static-root/account checks run first. The existing native control instrument then resolves\na live endpoint and digest-verifies its visible command surface. This issuer accepts only nonempty,\nbounded untargeted synchronous ephemeral surfaces. A targeted, journaled or action command refuses\nthe whole issuance before footprint work; unsupported rows are not silently dropped. The result\nrecords the observed responder and contract digests, not a promise that every future incarnation\nhas the same implementation. The caller is scoped to the class's observed command names, not that\nsingle answering instance.\n\nThe native agent provisioner creates the new lifecycle's bind-only footprint and additional endpoint\nrequest rows. No spawn, run or admin capability is added. The ordinary native agent baseline remains;\nthis is not a new hand-written permission profile or a managed-seat launch adapter. User-auth/open\nspaces still refuse. The selected broker's required TLS is retained for discovery and provisioning.\n\nAn immutable owner-private `<out>.issuance.json` intent precedes the first footprint operation.\nIt contains public identity/lifecycle, requested scope and contract coordinates, never credentials.\nIts presence is not a grant or successful submission receipt. An existing output or retained intent\nblocks another issuance, even if no output was published. Partial/unknown provisioning retains that\noriginal intent and does not replay the native call. Success reports native footprint acknowledgement\nand the exact privately published file; provisioning is not presence, task completion or cleanup.\nTemporary provisioner close is reported separately and can leave a partial result.\n\nRich/plain receipts show the actual caller/lifecycle, described endpoint owner and responder,\nexplicit channel scope and native expiry. Command tables show at most 40 rows with an omission\ncount; JSON retains every command and contract digest. The local issuance request is not a broker\nrequest acknowledgement.\n\nNo credential bytes or environment-export recipe are printed. Endpoint describe/invoke consume\nthis paired file under the same registered static root:\n\n```bash\ncotal ep describe ep:<class> --space <space> --creds /absolute/private/caller.creds\ncotal ep invoke ep:<class> <command> --space <space> --creds /absolute/private/caller.creds --args '<json>'\n```\n\nKeep the matching `<private-file>.issuance.json` beside the credential. The consumer checks both\nprotected snapshots and validates the selected root's full static trust chain. The native JWT\nsignature, matching user seed, current validity window, account and current delegated signing key\nare checked before connecting. A retained older signing key is not guessed from a different root.\nThe caller coordinates and recorded commands must reproduce the signed publish/subscribe/response\npermissions and principal tags through the native builder. The unsigned local intent selects that\ncomparison; it grants nothing and proves no submission, lifecycle currency or completion. Original\nresponder/contract metadata remains an issuance observation, not an instance or future-code pin.\n\nThe connection retains registered TLS and uses only that file. Its broker admission is separate\nfrom local checks. Invocation accepts only an included untargeted synchronous ephemeral method\nfrom the fresh digest-verified surface. A changed targeted, journaled or action shape refuses before\npublication. Instance pins, scatter, admin, agent targeting and goal-follow flags are unsupported\nfor this caller mode. Calls do not repair a binding, switch authority or resubmit an unknown effect.\n`--server`, when supplied, must repeat the registered broker. Target/reference selection is\nread-only, including when a paired file is selected. Receipts retain the public caller binding;\ndescribe shows methods outside the file scope without granting them. Issuance next steps keep\nexplicit file selection instead of switching back to the normal operator instrument.\n\nArbitrary raw files, issued-generation files and different native profiles are not interpreted as\nthis standalone caller. User-auth issuance and managed static launch are not supported.\n\n## Login\n\n```bash\ncotal login --idp <auth base URL> [--client-id <id>] [--plain]\ncotal logout --idp <auth base URL>\n```\n\nSigns you in to a per-user-auth mesh's IdP (device code flow) and caches the session; run it\nonce per machine. `--plain` presents the approval prompt and signed-in result without color.\nDevice approval is still interactive and has no native noninteractive or JSON result in this\nclient; `--no-input` refuses before starting a provider request. The IdP URL must use `https://`.\nPlain `http://` is accepted only on a loopback IP literal such as `127.0.0.1` or `::1`, for an IdP on this machine. `localhost` is refused because it\nis a name: a hosts entry would choose the IdP. It prints your IdP subject, the id the operator\ngrants against. When the trusted\n`/token` response advertises a same-origin space catalog, login validates and records that account's\nspaces immediately. After a\nlogin, every command on that mesh works under your identity: each connect takes a fresh IdP\nproof, exchanges it locally for a short-lived bearer, and is authorized against the actor\nledger at connect time. A provider that cannot be reached, or that stops answering, refuses with\n`DEPENDENCY_UNAVAILABLE` (exit 2) and caches no session. `logout` revokes the IdP session, clears\nits cache, and removes only that\naccount's discovered registry entries. See\n[identity & auth](identity-and-auth.md).\n\n## actor\n\n```bash\n# an upsert of the WHOLE row: name all three ACL flags, or pass --full for the wide defaults below\ncotal actor grant <actor> --sub <IdP subject> --scope a,b --allow-subscribe a,b --allow-publish a,b [--role <r>] [--label <l>]\ncotal actor grant <actor> --sub <IdP subject> --full [--scope a,b] [--allow-subscribe a,b] [--allow-publish a,b] [--role <r>] [--label <l>]\ncotal actor revoke <actor> (--sub <IdP subject> | --owner <u_\u2026>)\ncotal actor list\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | the resolved mesh | Space whose ledger to manage |\n| `--sub <subject>` | none | The IdP subject (shown by `cotal login`) the actor belongs to |\n| `--owner <u_\u2026>` | none | The derived owner token (alternative to `--sub`) |\n| `--full` | off | Fill each ACL flag left off with its wide default; without it, `grant` refuses unless all three are named |\n| `--scope <a,b>` | `spawn,role:default` with `--full` | Capability scope (`''` = none; `spawn` = may run agents; `role:<r>` = may delegate role r; `admin` = cross-agent control; `supervise` = eligible for the closed remote manager-service view when the host enables it) |\n| `--allow-subscribe <a,b>` | `>` (all channels) with `--full` | Channel read ACL; the user's envelope, their agents can never read beyond it |\n| `--allow-publish <a,b>` | `>` (all channels) with `--full` | Channel post ACL; also the envelope for their agents' posting |\n| `--role <r>` | none | Role (scopes the task-queue consumer) |\n| `--label <l>` | none | Display label for `actor list` (never the IdP subject) |\n| `--parent <owner.actor>` | none | Audit link to the spawning principal. An operator grant is the authority, so the link does not narrow the row |\n\nThe actor ledger is the single authorization source of a user-auth space: no row, no access.\n`actor list`, `grant` and `revoke` resolve their space and root as `auth grants`, `auth grant` and\n`auth revoke` do, and refuse the same way. With no space to resolve they refuse with a next step,\nsuch as `cotal up`, rather than assume one, and a root that does not hold the space's user-auth\nledger refuses `list` rather than report no rows. On a static or open space the refusal says that\nactor grants apply only to a space started with `cotal up --user-auth`.\nA usage error refuses with exit 1 before any space or authority is read, and one refusal names every\nmissing input: `--sub` or `--owner`, and for `grant` the ACL flags left off without `--full`.\n`grant --full` is the **full** envelope (all channels; scope `spawn,role:default`, so it may spawn and may delegate the default role). A\n`grant` that leaves off `--scope`, `--allow-subscribe` or `--allow-publish` without `--full` is\nrefused and writes nothing. A re-grant **replaces the whole row**, not the one field you name, so to add a capability spell\nevery field out: the new scope plus the row's current read set, post set, role and label\n(`cotal actor list` shows what a row holds). Under `--full`, a field left off does not stay as it\nwas: it reverts to the wide default in the table above. A re-grant retires the current interactive lifecycle through the running auth\nservice before it rotates the row, so copied bearers cannot cross an authorization update. If that\nretirement cannot be confirmed, the row is left unchanged and the command fails with the recovery\naction. `revoke` uses the same retirement before deleting the row, which lets a later grant create a\nreal successor instead of colliding with a live predecessor. `supervise` is separate from `spawn` and `admin`: it only makes a signed-in\nperson eligible for the host-provided closed remote manager-service view; it does not grant\nmanagement of another owner or a general host profile. `revoke` denies the next exchange and\nthe next connect with no restart, and evicts the principal's live connections. It reports\n`verified gone` only when the delivery daemon's eviction result names the principal and shows a\ncomplete scan with no connection left; any other result prints `live-connection eviction INCOMPLETE`\nwith its counts, or with the reason the result was not accepted. Managed-agent rows\n(written by the spawn path) live in a disjoint row space this command never touches. See\n[identity & auth](identity-and-auth.md).\n\n## doctor\n\n```bash\ncotal doctor auth [--space <space>] [--fix] [--json|--plain]\n```\n\nThis inspects the current root's local credential claims and recorded renewal state. It does not\nprove current broker authorization, daemon adoption, connected agents or task progress. The shared\nreport keeps full credential paths and public issue/expiry metadata; JSON retains all diagnostic\nrows without JWTs, seeds, fingerprints or raw parser/renewal error bodies. On a root that holds several\nspaces, `--space` names one; it defaults to the sole space. Without `--space`, a root with several\nspaces refuses with `TARGET_AMBIGUOUS` (exit 2) and names them. In a folder whose root holds no space,\nthe report is partial (exit 4) and its next step is to run it in a space's root, which `cotal space`\nlists.\n\nPlain diagnosis starts no repair or connection. A required legacy credential or user-auth directory\nmigration refuses instead of silently moving files or reading canonical absence as an empty scope.\nA credential with no expiry claim is a finding whatever its kind, since a copy stays valid until its\nsigner rotates. `--fix` re-signs only the static daemon credentials, so an unbounded agent credential,\nsuch as one `auth mint` wrote without `--expires-in` or `--expires-at`, stays a finding until it is\nreplaced with a bounded one.\nMissing local signing trust is not inferred to mean an open or healthy mesh. An unreadable renewal\nrecord prevents a complete healthy verdict. Locally ordered older incarnation files are labeled as\nfile observations, never proof of a live successor or permission to delete or respawn an agent.\n\n`--fix` explicitly permits native local material migration and the supported static daemon re-sign\nphase. Its path-move and re-sign acknowledgements are retained separately. It holds the native\nrenewal lease when using the broker and refuses another holder. Offline repair is labeled as\nuncoordinated; an authenticated preflight failure is not called an offline target. User-auth roots\ncannot receive a static-signer repair fallback. No system account is rotated and no agent is\nrespawned or silently given a new identity.\n\nA local re-sign does not erase a recorded broker refusal or claim that a daemon adopted it.\nA recorded refusal names the space's manager, the renewal owner, as the next step. A pre-per-space\n`.cotal/renewal.json` names no space, so it is reported as a leftover and never read as the space's\nverdict.\nRemaining findings, incomplete audit reads and partial/unknown repair outcomes keep classified\nfinite receipts. Inspect the original renewal owner and lifecycle rather than repeating a mutation\nor creating replacement agents merely to obtain a different report.\n\n## join\n\n```bash\ncotal join --space <s> --name <n> [--role <r>] [--channel <c>]\ncotal join --link <url> | --token <t>\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which mesh, and which credential |\n| `--name <n>` | none | Your presence name |\n| `--role <r>` | none | Your role |\n| `--channel <c>` | none | Channel to join |\n| `--kind <k>` | `agent` | Endpoint kind |\n| `--link <url>` | none | Join link (`cotal://\u2026`) |\n| `--token <t>` | none | Join token |\n| `--lifecycle-uid <uid>` | none | Required with `--creds`: the lifecycle UID minted alongside the credential (`COTAL_LIFECYCLE_UID` works too). A credential's durable grants name exact lifecycle-keyed resources, so `join` refuses to invent one |\n| `--tls` | off | Connect over TLS |\n\nAn interactive presence: join a space under your own name and role, without launching an agent\nharness. A `--link` or `--token` supplies the where and the auth in one value. See\n[Spaces](spaces.md) and [Identity and auth](identity-and-auth.md).\n\nOn a registered user-auth space, sign in with `cotal login` first. `join` uses your existing `cli`\nactor bearer and renews it through the same login and exchange while the session runs. Its channel\nand DM resources must already be provisioned by the space operator. It never mints static\ncredentials or widens your grants. A changed principal, lifecycle or connection trust refuses\nrenewal and requires leaving and joining again. A revoked login is reported by renewal, and the\ncurrent connection can last only until its already-issued bearer expires.\n\nFor `nats://` and `tls://` addresses, if TLS is required but the broker does not offer it, `join`\nrefuses with a request for the operator's TLS-enabled broker address. It never retries over\nplaintext. A `ws://` address uses plaintext WebSocket. Use `wss://` for TLS.\n\n## Manifest deploys\n\nA `cotal.yaml` manifest declares a whole mesh (channels, personas, roles, and ACLs) in one file.\nThese commands consume it:\n\n```bash\ncotal up -f cotal.yaml # boot a fresh mesh from the manifest\ncotal team deploy cotal.yaml # deploy the manifest additively onto a running mesh\ncotal team remove <deployment-id> # retire a returned deployment\ncotal topology view -f cotal.yaml # validate + view the access graph, change nothing\n```\n\n`up -f` brings up a new broker and applies the manifest. `team deploy` requires a registered,\nrunning mesh and applies additively: all-new live channel-only manifests, or bounded all-new agent\nand mixed manifests on static-auth or open meshes through one native manager. It does not launch\nuser-auth agents. `team remove` retires a returned deployment by its full ID within the limits in\n[Recorded team views](#recorded-team-views). `spawn -f` no longer deploys a manifest; an additive\nledger written by that older path is torn down with `cotal down -f <file> --run <id>`. `up -f`,\n`team deploy` and `team remove` take `--dry-run` to print the plan without mutating anything.\n`topology` validates the manifest and renders its channel / role / ACL graph. See\n[Define a team](define-a-team.md) and the [manifest reference](manifest.md).\n\n## ext\n\n```bash\ncotal ext [list] [--json | --plain]\ncotal ext add <npm-package>\ncotal ext remove <name>\ncotal ext root # print just the install prefix (scriptable)\ncotal ext seed [--repair|--reset|--force]\n```\n\nOperator-installed extensions: `add` installs an npm package into a cotal-owned prefix and records\nevery registry provider it contributes. Commands appear in help, completion, and dispatch; runtime\nproviders are lazy-loaded by commands such as `supervise`; local process providers participate in\n`status` and selective `down`. `remove` and `list` manage them. The `@cotal-ai/web` dashboard is the\ncanonical command/process example. Installed packages and their location are described in\n[config](config.md). When a package needs an export its linked `@cotal-ai/*` peer does not have,\n`add` rolls back and names which install is behind, as a later load of an installed one does.\n\nBare `cotal ext` lists the inventory, headed by the install prefix. That prefix is a cotal-owned npm\nroot kept **separate** from npm's own global tree. These packages never show up in `npm list -g`,\n`cotal ext` (or the Extensions section of `cotal status`) is the canonical inventory. `cotal ext root`\nprints only the path, for scripts. The versions shown are the manifest pin recorded at add time.\nOn POSIX, the extension writer creates a missing Cotal config directory with mode `0700`, even\nwith a group-writable umask, and a missing install prefix with mode `0755` and its `package.json`\nwith mode `0644`. Its npm children start with 022 added to your umask. Existing directory\npermissions are not changed. Completion still refuses a pre-existing writable ancestor rather than\nweakening its shell-file ownership checks.\n\nBare `ext --json` (or `ext list --json`) writes one JSON document: `result.root` is the install\nprefix and `result.items` holds one row per installed extension with `pkg`, `version`, `spec` (what\n`ext add` was given), `seeded` (`true` for an entry the built-in seed installed) and `provides` (the\n`kind:name` refs the table shows). An empty prefix is an empty list. `--plain` writes the same report\nwithout color. The other `ext` subcommands refuse `--json` and `--plain` with `USAGE_INVALID`.\n`ext root` stays a raw path for `$(cotal ext root)`.\n\nAn installed package whose directory is gone is reported as not installed. One whose `package.json`\nis not JSON or has no `version` is reported as corrupt, naming the package and its `package.json`.\n\nRemoving an extension that owns a running local process is refused with the mesh root and its\n`cotal down <component>` command; stop it first so uninstalling the package never strands a process\nwhose lifecycle provider is gone.\n\nLoading an installed extension takes the same locks as `add`, `remove` and `cotal update`. When another\n`cotal` process holds them, the load waits up to five seconds in total (one deadline for both locks)\nwithout blocking the waiting process, then refuses. A running `cotal update`, `cotal ext add` or\n`cotal ext remove` holds the update-pass lock as well as the install/remove lock, so the load refuses with\n\"another extension update or mutation is in progress (pid N) - retry once it finishes\". The install/remove\ntext, \"extension install/remove is in progress (pid N) - retry after the active `cotal ext` command\nfinishes\", appears only when the update-pass lock is free but a live process still holds the\ninstall/remove lock itself, for example an update child that outlived its `cotal update` parent. Re-run\nthe command once the other one has finished.\n\n### Built-in connectors are seeded extensions\n\nThe first-party agent connectors (`claude`, `opencode`, `codex`, `hermes`, `jcode`, `pi`) are not compiled into\nthe binary. They are seeded on first run through the **same** `ext add` path a third party uses, and\nappear in `cotal ext list` like any other extension. So you can remove one you do not want\n(`cotal ext remove @cotal-ai/connector-hermes`), and a deliberately-removed connector STAYS removed\nacross upgrades. `cotal ext add <your-package>` adds a third-party connector the same way. The web\ndashboard (`@cotal-ai/web`, providing `service-preset:web`) is the seventh built-in seeded on the\nsame path.\n\n`cotal ext seed` is the maintenance entry for that seeding (it runs automatically before the first\ncommand of each boot that is not a read, so you rarely call it). A read, such as `status`, `ps`,\nbare `use`, `ext list` or `docs`, installs and refreshes nothing. An installed extension's `reads` declaration is\ncached when it is added. A bare read and a read subcommand skip that seed prelude; a subcommand\nwithout the declaration still seeds. After an upgrade, a read that would load a\nbuilt-in still at another version names the package and its version with `cotal ext seed` instead\nof loading it. Each seeded connector's `\u2713 added` line goes to stderr, so the command that triggered\nthe seed keeps stdout to itself:\n\n| Flag | Meaning |\n|---|---|\n| (none) | Reconcile: seed any never-seeded built-in, refresh a seeded one whose version the binary bumped, leave a removed one removed. A no-op once current. |\n| `--repair` | Recover after an interrupted seed or a lost authority (rebuilds the interrupted connector; restores the removed-vs-never-seeded record from its durable backup). Also reinstalls a seeded built-in whose entry file is gone or whose `package.json` is not JSON or has no `version`. |\n| `--reset` | Discard the record and re-seed all seven built-ins (the six connectors plus the web dashboard). **Resurrects any you removed.** Rebuilds cleanly over corrupt seed state. |\n| `--force` | Re-seed the built-ins even when the version stamp is current. Refuses to downgrade a newer shared store. |\n\nWhen a newer `cotal` advances the operator-global seed store to its generation, it prints one\nmigration line naming the old and new generations, the exact CLI entry that wrote the store, the\ncommit timestamp, and `seed/stamp.json`. That writer and timestamp are kept in the stamp, so a later\nolder CLI refusal can say which executable wrote the generation it will not overwrite and when.\nLegacy generation-only stamps remain readable; their refusal simply has no writer provenance to add.\n\nAn older `cotal` refuses a seed store written by a newer version. When it can verify a sufficient\n`cotal` executable on PATH or at the installer's `~/.local/bin/cotal` location, the refusal names\nthat absolute path so a reduced service PATH does not select the older binary again. Otherwise it\nkeeps the generic newer-version instruction. Every maintenance mode, including `--force` and\n`--reset`, refuses to replace a newer shared generation. Run the newer executable instead.\nSame-generation force remains supported. Reset still repairs corrupt state and resurrects\nremoved connectors. Older released executables may still allow a backward rebuild; do not use\ntheir force advice to recover from selecting the wrong installation.\n\nA source-checkout CLI (`pnpm cotal`, `tsx bin/cotal.ts`, `node bin/cotal.ts`, or a suite child of\nthose) refuses to write or garbage-collect that store. The refusal names the path, the generation\nit declined, and `COTAL_SKIP_CONNECTOR_SEED=1` as the way to run other commands from a checkout,\nbecause pointing `$XDG_CONFIG_HOME` at a scratch dir alone does not lift it. With the skip set, a fresh\nconfig gets no built-in connectors (`cotal ext list` shows none), so add one from the checkout with\n`cotal ext add <checkout>/extensions/connector-claude-code` against that scratch `$XDG_CONFIG_HOME`. `COTAL_HOME` does not relocate this\nstore. An entry that cannot be proven as a released install is refused the same way. Isolated\nrelease tests that must seed from a checkout-shaped `bin/` set `COTAL_ALLOW_CHECKOUT_SEED=1` after\npointing `$XDG_CONFIG_HOME` at a scratch dir; that override is documented here, not on the refusal\nline. An opt-in write still records the checkout path in `seed/stamp.json` as `writtenBy`.\n\nThe default connector for a bare `cotal spawn` (no `--agent`) is the persona's `agent:` pin if it\nhas one, else `claude`; set `COTAL_DEFAULT_AGENT` or `cotal config set harness` (e.g. `opencode`)\nto change the fallback. It is a default, so a persona that pins its harness still wins over it. An `--agent` naming a removed\nconnector fails loud with the exact\n`cotal ext add` to restore it. Set `COTAL_SKIP_CONNECTOR_SEED=1` to turn off the automatic first-run\nseed/refresh entirely (for a controlled or offline setup that manages connectors by hand); `cotal ext\nseed` still runs on request. `cotal agent-bearer` never takes the seed at all: it is exec'd by\nspawned seats on every bearer refresh, so it neither reconciles nor is refused by the store's\ngeneration (see [Plumbing](#plumbing)).\n`cotal completion` skips the seed as well, so a shell profile that sources its stub never reconciles\nand is never refused by the store's generation or the source-checkout guard. Printing a stub writes\nnothing, and `completion install` writes only shell configuration.\n\n## docs\n\n```bash\ncotal docs [<page>] [--refresh] [--json | --plain]\ncotal docs --search <query> [--json | --plain]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--search <query>` | none | Show the sections that best match a query, each naming its page |\n| `--refresh` | off | With a page: also try the version-pinned copy on docs.cotal.ai |\n| `--json` / `-j` | off | One JSON document |\n| `--plain` | off | The same report without color |\n\nReads the docs bundled with this installed version, offline: the same pages agents read with\n`cotal_docs`. Bare `docs` lists the pages by category (`Start here`, `Concept`, `Guide`, `Reference`,\n`Project`), each with its title; `result.pages` holds each page's slug, title, kind and summary, and\n`result.sources` the normative `spec`, `lang` and `schema`. A page is named by its slug,\nignoring case and a trailing `.md`; `result.body` holds its Markdown. An unknown page is refused with\n`USAGE_INVALID`, naming every page.\n\n`--refresh` fetches the page from the immutable path for this version on docs.cotal.ai.\n`result.source` says which copy was served, `bundled` or `docs.cotal.ai`, and `result.refresh` says\nwhat the fetch did: `served`, `not-published` (the path has no copy) or `unreachable`. The bundled\ncopy is served whenever the fetch does not succeed. `--refresh` applies only to a page.\n\n`--search` ranks sections by the words of the query. A search with no match is an empty result.\n\n## config\n\n```bash\ncotal config [--user | --space <name>] [--json | --plain]\ncotal config get <key> [--user | --space <name>] [--json | --plain]\ncotal config set <key> <value> [--user | --space <name>] [--json | --plain]\ncotal config unset <key> [--user | --space <name>] [--json | --plain]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--user` | your defaults for a write | Your own values, with no space's values |\n| `--space <name>` | the selected space for a read | Your values for one space |\n| `--json` / `-j` | off | One JSON document |\n| `--plain` | off | The same report without color |\n\nYour [preferences](config.md#preferences): `harness`, `persona`, `detach-key`, `color` and `editor`.\nBare `config` lists every key with the value a command uses, where it came from (a variable, your\nvalue for the space, your own value, or the default) and the values it outranks. `get` shows one\nkey. A read looks through `--space`, else the space selected with `cotal use`, else your defaults;\n`--user` reads your defaults alone.\n\n`set` and `unset` change your defaults, or your values for one space with `--space`. Only `harness`\nand `persona` have per-space values. A write that a variable outranks in this shell is still saved,\nwith a `PREFERENCE_SHADOWED` warning naming the variable. Setting a value that is already saved, or\nunsetting one that is not, is a `no-op` and exits 0. An unknown key or an invalid value is refused\nwith `USAGE_INVALID` and the valid choices, and a write that lost a race with another writer is\nrefused with `CONFLICT`.\n\n`result.settings` (or `result.setting` for `get`) holds each key's `value`, its `winner`\n(`{source, name, value}`, where `source` is `environment`, `space`, `user` or `default`) and the\n`shadowed` layers below it. A write's result has the `previous` and new `value`, the file's new\n`revision`, and `effective`, the key as a command there now resolves it.\n\n## completion\n\n```bash\ncotal completion [--json|--plain]\ncotal completion script <bash|zsh|fish|powershell>\ncotal completion install [shell] [--json|--plain]\ncotal completion uninstall [shell] [--json|--plain]\n```\n\nBare `completion` reads local integration files. It reports the script version, startup reference\nand recorded ownership separately. It does not inspect or load the running shell. An unreadable\nfile or ownership record produces a warning and a partial result, exit 4. A relative `ZDOTDIR`\nis resolved from the current directory for this report, as zsh resolves it.\n\nIn zsh, initialize its completion system with `autoload -Uz compinit && compinit` before\nsourcing the native zsh stub.\n\nLoad completion in the current shell:\n\n```bash\nsource <(cotal completion script bash)\nsource <(cotal completion script zsh)\ncotal completion script fish | source\ncotal completion script powershell | Out-String | Invoke-Expression\n```\n\n`completion script` writes raw shell code and takes no `--json` or `--plain`. Generation is\noffline. The stable hidden `__complete` entry reads registered command specifications and local\npersona files. It does not contact the mesh or refresh extensions.\n\n`install` and `uninstall` take a shell name or detect it from `SHELL`. Pass `powershell`\nexplicitly when PowerShell inherited another shell's `SHELL`. On supported POSIX hosts:\n\n- Bash uses `~/.config/cotal/completion.bash` and an owned block in `~/.bashrc`.\n- Zsh uses `~/.config/cotal/completion.zsh` and an owned block in `$ZDOTDIR/.zshrc`, or\n `~/.zshrc` when `ZDOTDIR` is unset.\n- Fish uses `~/.config/fish/completions/cotal.fish`, which fish loads automatically.\n- PowerShell on Linux or macOS asks the installed `pwsh -NoProfile` for its current-user\n all-hosts profile and wires an owned `completion.ps1` stub into that file. A missing shell,\n invalid profile reply or missing profile directory refuses without guessing a path.\n\nAn absolute `XDG_CONFIG_HOME` replaces `~/.config` for the owned stubs; a relative value is\nignored. Mutation refuses a relative `ZDOTDIR`, missing startup directory, symlink, untrusted\nowner or unsafe file access. A startup file its owner made read-only is never replaced. `install`\nstill writes its stub, then fails, exit 5, and names the line to add to that file yourself.\n`uninstall` refuses it, exit 2, and changes nothing until you remove that line. On Windows, PowerShell script generation works but automatic\nprofile editing remains unsupported: the guarded POSIX file writer cannot safely replace\nWindows reparse-point paths. Installing files does not establish that the host runs its\nprofile or permits scripts under its execution policy.\n\nInstallation writes a private `.ownership.json` record beside its stub. Reinstall refreshes only\nan unchanged recorded stub. Uninstall removes only that stub and the unchanged recorded startup\nblock, keeping other startup bytes. Modified or unrecorded material is preserved with a refusal.\nA stub from an earlier release without an ownership record needs manual review; matching\ngenerated text alone is not treated as ownership. An unowned startup reference to a managed stub must be reviewed\nbefore removing the stub. These commands cannot unload functions in a parent shell.\n\nCotal writers use a per-shell lock and recheck file identity and bytes before changing them.\nAn unconfirmed filesystem mutation is reported as unknown and is not retried. A confirmed change\nfollowed by another refusal is partial, with the acknowledged file paths retained. This is not\na filesystem compare-and-swap against an editor that ignores the lock.\n\n`update` refreshes existing unchanged recorded stubs through its current-generation CLI. It never\nadds startup wiring or reinstalls a removed stub. Modified and unrecorded integrations are kept;\nany guarded refresh it cannot complete is reported. `update --check` does not refresh completion.\n\nThe old `completion <shell>` form is a usage error. Custom startup commands need manual migration;\nsee [Upgrading](UPGRADING.md#shell-completion-script-in-0910). No command silently\nrepairs custom wiring. Install and uninstall support one finite JSON document or the plain report.\n\n## feedback\n\n```bash\ncotal feedback \"<summary>\" [--type <type>] [--email <contact>] [--details <text>] [--json|--plain]\ncotal feedback \"<summary>\" --key-file <private-file> [--json|--plain]\n```\n\nThe type is bug, idea, friction, praise or other; severity is low, medium or high. Optional area\nand contact name are included only when supplied. The public intake requires a traceable email,\nfrom `--email`, the existing configured contact or local git email. No environment setup is\nrequired. Do not put credentials, private transcripts or unrelated customer data in feedback.\n\nA protected `--key-file` selects keyed intake without placing a credential on argv. An existing\ninternal intake configuration remains supported. The old raw `--key` input refuses before the\nrequest and does not echo its value. Private key files must meet the shared file-ownership and\nprotection policy; unsupported environments refuse rather than weakening that policy.\n\n`--url` can select a self-hosted intake. Remote intake requires HTTPS; HTTP is limited to literal\nloopback development hosts. URL credentials and fragments are refused, and redirects are not\nfollowed. One request has a 20-second deadline and is never automatically retried or rerouted to\na different credential/intake. A network or unconfirmed server outcome can mean the report\narrived: inspect the original intake before sending it again.\n\nJSON/plain receipts report the intake origin, HTTP acceptance and a bounded record ID when the\nserver provides one. They do not repeat the submitted summary, details, contact values, key or\nresponse body. Acceptance does not prove that anyone read the report or acted on it. A success\nresponse without an established ID is reported with that limitation, not retried for a new ID.\nRun a self-hosted intake with [`feedback-intake`](#server-daemons).\n\n## telemetry\n\n```bash\ncotal telemetry [--json|--plain]\ncotal telemetry off|minimal|regular [--json|--plain]\n```\n\nThe CLI sends anonymous usage data: one small event per command as it exits, and at the default\n`regular` level one install summary per day. [Telemetry](telemetry.md) lists every field and what\nis never sent. The bare form prints the level in force, what set it (the settings file,\n`COTAL_TELEMETRY`, `DO_NOT_TRACK` or `COTAL_TELEMETRY_DISABLED`), the install id, the intake URL and\nthe settings file. A level argument writes the file; `on` is an alias of `regular`, and writing the\nlevel the file already holds is a `no-op`. A level the environment forces still wins, and the\nreport says so. `cotal telemetry` itself sends nothing.\n\nThe first interactive run at `regular` prints one notice to stderr. `COTAL_TELEMETRY` set to\nanything but a level refuses `USAGE_INVALID`, and a settings file that does not validate refuses\n`SCHEMA_INVALID` on every command until it is fixed or moved away.\n\n## run\n\nOperate durable workflow runs (cotal-lang programs) from the terminal.\n\n```bash\ncotal run start --file <program> [--timeout <dur>] [--local --admit-read <channels> --admit-publish <channels>] [--on <instance>]\ncotal run resume <runId> [--local --file <program>] [--on <instance>]\ncotal run ps [--endpoint <ep>] [--json] [--on <instance>]\ncotal run ps --page --on <instance> [--limit <n>] [--cursor <cursor>] [--endpoint <ep>]\ncotal run journal <runId> [--endpoint <ep>] [--json] [--on <instance>]\ncotal run answer <runId> <stepKey> [--value <json>] [--artifact <ref>] [--endpoint <ep>] [--local --by <who>] [--on <instance>]\ncotal run amend <runId> <stepKey> [--value <json>] [--artifact <ref>] [--endpoint <ep>] [--local --by <who>] [--on <instance>]\ncotal run revoke <runId> --local --by <who> --reason <text> [--endpoint <ep>]\ncotal run migrate <runId> --local --file <program> [--endpoint <ep>]\n```\n\nEvery verb also takes `--space <s>`, `--server <url>` and `--creds <path>` to pick the mesh and the\ncredential.\n\nHosted run verbs accept `--on <manager-instance>` to choose one full manager id\nas `cotal ps` prints it. A managed seat defaults to the manager that launched\nit; an unbound user-auth caller with several managers must choose one. Without\n`--on` its call is refused with exit 2 before any request, and the refusal names\neach live manager.\n`--on` does not apply to local, offline or seat-only run commands, and a failed\npin never falls back to another manager. Rich run reports label an explicit pin\nas the requested manager, and the JSON target carries `requestedManagerInstance`.\nFollow-up commands printed by the CLI retain the same explicit pin and fill\nin a returned run ID only when it matches the native run-token shape. Keep the\npin on later status, answer and resume calls.\n\n`start` hands the program to the mesh's manager, which validates it, mints the run id (the record\nnever takes a caller-supplied one), drives it in its own process, and answers with the id once the\nrun is recorded; a program that does not validate is refused with every problem listed. `resume`\nasks the manager to take an existing run back and continue it from its step journal; the source is\nthe recorded program, so no `--file` is taken. Neither takes `--endpoint`: the manager records\nits runs under its own endpoint, and naming another is refused. `ps` lists the run records and\n`journal` renders one run's durable records; both only inspect. An open pause prints its question.\nA pause settled with an accepted answer prints its value as JSON plus the recorded answerer,\nartifact when present, time, and answer id, then one `amended` line per later amendment, in the\norder the store committed them, so the last is the current position.\nExpired pauses and ordinary steps print no answer line.\n`--json` on `ps` or `journal` prints each row the manager answers with (or `--local` reads) as one\nJSON object per line. A `ps` row carries `runId`, `endpoint`, `state`, `holder`, `epoch`,\n`journalHigh`, `forkedFrom`, `startedAt` and `programHash` (the values the program's `run()`\nreports; `programHash` is absent for a run with no recorded program), and `revoked` or\n`revocationUnreadable` when the marker says so. A `journal` row is an `activation` or a `step`. A\nstep row carries its `step` key, the `effect` kind and its `name`, `state`, `outcome`, the recorded\n`status` and `errorCode` once settled, and `startedAt` and `endedAt` in epoch milliseconds. An open\npause adds its `asks`, its `deadlineAt`, and for a checkpoint the `onExpiry` it was armed with; a\nsettled pause adds its `answer` and its `amendments`, as the text view prints them. A field the\njournal does not record is absent: a checkpoint opened before `onExpiry` was recorded carries none.\nThe run header and errors go to stderr, so stdout carries only rows; an unreadable revocation marker\nprints its reason there and still exits 1. The text view is presentation and is not a stable\nparsing target. `--json` on any other verb is refused.\n`ps --page`, which is not in release 0.88.0 or earlier, asks one manager for a single bounded\ndiagnostic JSON page instead of the full list:\n`--limit` takes 1 to 16 rows (default 8), and `--cursor` continues a scan from a returned\n`nextCursor`. It requires `--on` and refuses `--local`, and a page proves neither absence nor\nownership ([Diagnostic inventory](workflows.md#diagnostic-inventory)).\n`revoke` writes the run's revocation marker from the project folder; `--by` and `--reason` are\nrequired ([workflows](workflows.md)).\n`answer` resolves an open\ncheckpoint through the manager, presenting as the holder that armed it; the manager records the\nanswerer from your credential, so no `--by` is taken there. A settled step refuses a second\n`answer`. `amend` records a changed position on a settled checkpoint or `ask`: it files a new\nanswer beside the accepted one, naming it, and the journal lists it under the step. The pause stays\nsettled and the run keeps the answer it acted on. A step that is still open or settled without an\nanswer refuses an amend. A spawned seat may amend only an answer recorded under its own name. `migrate` runs the migrate check of an\nedited program against a run's journal, from this terminal under a read credential (`--local`\nonly; the manager serves no run-migrate command): it prints whether the migration is admissible,\nevery orphaned step with its verdict and code, and exits 0 on admissible and non-zero on not. It\nwrites nothing: the commit that would file the migration is not reachable yet, and the report\nsays so. `migrate` refuses `--adopt`, `--release` and `--discard-approvals`, since they decide what a\ncommit does and the check files nothing. `--timeout` sets the default\ncheckpoint timeout for a drive (default 1h). `--on <instance>` sends a hosted verb to one manager\ninstance, with the same rules as [`ps`](#managed-seats). On a user-auth mesh where you run more than one\nlive manager, an unpinned hosted verb is refused and `--on` picks one. `--local` takes no `--on`. `--local` drives in this process instead, over one\nconnection per invocation under the run's own credential minted from the project folder's trust\nmaterial, and is the path on a bare broker with no manager or for a run with no recorded program\n(`cotal run resume <runId> --local --file <program>`); `answer --local` and `amend --local` take\n`--by <who>`, and `start --local` requires `--admit-read` and `--admit-publish` (comma-separated\npatterns or `none`), the run's channel ceiling. On a\nuser-auth mesh the host's own manager refuses the family by name, and `--local` has no credential\nthere. A participant's manager started with `cotal supervise` hosts a logged-in user's runs through\nits issuing host: the auth callout issues the user's manager connection, and every `run` verb rides\nthe versioned rail under that issuance.\n[User-auth run start](design/user-auth-run-start.md)\nrecords the path. The guide is [workflows](workflows.md).\n\n## Server daemons\n\nTwo long-lived infra roles ship with the CLI. They are not part of everyday operation; the delivery\ndaemon comes up automatically with `cotal up --detach` in auth mode.\n\n`deliver`, `auth-service` and `feedback-intake` are deprecated entries kept for existing units and\nscripts, as is `supervise`; help and completion omit them. Each accepts its existing flags and\nprints a warning on every run naming its service preset and the release that removes it. The\ndelivery daemon and auth service that `cotal up` starts run the same code through hidden entries\nand print no warning. New setups use `cotal service add <name> --preset <preset>`; see\n[deprecated daemon entries](UPGRADING.md#deprecated-daemon-entries).\n\n```bash\ncotal deliver --space <s> [--server <url>] [--creds <file>] [--root <dir>]\ncotal auth-service --space <s> --server <url> [--port <n>] [--exchange-public-port <n>] [--exchange-public-url <https://\u2026>] [--exchange-trusted-proxy] [--advertised-server <url>] [--agent-provisioning-url <https://\u2026>]\ncotal service add intake --preset feedback-intake --space <s> --root <absolute-root> --keys <private-keys.json> --creds <private-creds>\ncotal service start intake --space <s>\n```\n\n`auth-service` runs a user-auth space's identity plane: the NATS auth callout, the\ncapability-gated local exchange and JWKS, and, when `--exchange-public-port` is set, the closed public\nexchange/discovery face forwarded by an HTTPS reverse proxy. `--exchange-public-url` is the proxy URL\nadvertised to clients; `--exchange-trusted-proxy` opts into last-hop `X-Forwarded-For` attribution.\n`--advertised-server` and `--agent-provisioning-url` set the broker address and agent-provisioning\nendpoint the public bundle advertises, as on [`up`](#up).\n`cotal up --user-auth` starts and supervises the service for you, so you run it directly only to\nrecover one by hand.\n\n`deliver` runs the server-side Plane-3 delivery daemon: the durable backstop and membership/ACL\nauthority. It is auth-mode-only and single-instance (`--shard`/`--shards` accept only `N=1`);\n`--dev-mint` mints a scoped cred for standalone dev, signed by the local account of the space the\ndaemon serves. `--creds` can start a daemon that already looks healthy, but production renewal is\nnot that file alone: the manager and the daemon must address one credential store. On a stock split host with two project roots, a\ndirect `deliver` is not an independent repair; keep the daemon under `cotal up` on the broker\nhost, or inject the same store into both processes ([embedding](embedding.md#supervisor-signing-authority)).\nTyped by hand on the workstation, `deliver` dials the broker recorded for `--space` in the mesh\nregistry (a mismatching `--server` is refused before any dial, and a record for a different\nworkspace root is refused outright); with no record for the space it falls back to the local mesh.\nThe daemon serves the workspace root that `--root <dir>` names, which must hold `.cotal/`, or else\nthe nearest `.cotal/` above its working directory. With neither, it refuses at start and names the\ndirectory it searched from, before it reads a credential or dials a broker.\nSee the [delivery daemon](delivery-daemon.md). `feedback-intake` runs a self-hosted feedback server\n(requires `--keys` and a scoped `--creds`), announcing submissions into a space channel; flags\ninclude `--host`/`--port`, `--store`, `--space`/`--channel`, `--max-bytes`, and `--rate-limit`.\n`--port` takes a decimal port from 1 to 65535 and is checked before the intake dials the broker.\n\n## Plumbing\n\n`cotal __complete <words\u2026>` is the internal entry the shell-completion stubs call to emit candidates\nfor the current command line; you never run it directly. `cotal agent-bearer` is machine-facing\nplumbing on user-auth meshes: spawned agents exec it to print a fresh short-lived bearer from their\nspawn-time secret; you never run it directly either. Its local arm uses `--dir` to discover the\ncapability-gated loopback service. A remotely enrolled, already-granted agent instead receives\n`--exchange-url <https://base>` in its launch argv: that arm sends `{owner, actor, actorToken}` to the\npinned public exchange with no local capability, follows no redirects, and refuses every non-HTTPS\nURL because the actor token is the credential in the request body. Because a seat execs it on every\nbearer refresh, it skips the connector-seed boot gate entirely: it reads one 0600 token file,\nexchanges it and prints the bearer without consulting or writing the operator-global seed store, so\na newer store generation cannot refuse a live seat's refresh. For the same reason the seat's command\nnames the CLI entry file the `cotal` bin link resolved to at spawn, not the link: an install that\nlater replaces the link, such as the get.cotal.ai launcher over an `npm i -g` link, leaves running\nseats able to refresh. `--manager-call` asks for the\ninstance-bound `manager-caller` view; `--manager-instance <id>` selects an explicit live candidate,\nand an owner with more than one live candidate is refused with their ids until it names one.\nThat mode still prints only the raw token and does not update `--health-file`. A spawn runs it once as the agent auth\npreflight. When it fails there without printing a sentence of its own, the refusal names the cause: the 30 second\ntimeout, the signal that killed it, or its exit code. (`cotal start` is a removed tombstone: it\nerrors and points you to `cotal spawn --detach`.)\n\n`cotal app-identity` is plumbing for a Claude app conversation, and you never run it directly\neither. The Claude connector's local server runs `cotal app-identity provision --name <name>` when\na conversation connects. It picks the mesh as `cotal spawn` does (`--space` selects one) and prints\none JSON line. A new identity opens only after a person approves it at the mesh's authority, so\nprovision needs a signed-in mesh whose authority advertises connection approval v1, and the\nconversation's native `--session`. It keeps the request and its secret in a private record under\n`~/.cotal/app-identities`. Until the person decides, it answers\n`{\"ok\":false,\"pending\":true,\"requestId\":\"<id>\",\"approval\":{\"id\":\"<id>\",\"url\":\"<url>\",\"expiresAt\":\"<time>\"},\"proposed\":<selection>}`,\nwith no credential, secret or path. `proposed` is `null` for a request without a selection.\nProvisioning again sends the same request: it answers the same approval while pending, and the\nlaunch env and the reference that `retire` takes once approved. The grant is what the person\napproved, which can differ from the request. A denial or an expiry drops the record, and the next\nprovision makes a new request. `--selection <json>` proposes channels, a role and capabilities for\nthat conversation. A mesh with static credentials, an open mesh, and a signed-in mesh whose\nauthority this machine holds have no approval page, so provision opens no identity on them.\nThe connector passes `--require-approval`. Every provision needs approval with or without it, and\nan earlier CLI that opened identities without approval refuses the unknown flag before doing\nanything. A saved request that was never issued needs the person's decision even when an earlier\nversion saved it.\n`cotal app-identity retire --dir <dir>` still removes an identity an earlier version opened on a\nstatic mesh: its broker footprint, then its files and directory. When the broker is not answering it\nkeeps them. Each provision also retires those directories whose owning process has exited. For a\nsigned-in identity, `retire` keeps the record until the mesh confirms retirement, and names the\nmesh's phase while it is pending. A request still pending is dropped.\n`cotal app-identity reap` retires the selected mesh's saved conversations whose server process no\nlonger exists. The connector runs it after each signed-in connection, so a connection never waits\nfor that cleanup. Like `agent-bearer`, it skips the connector-seed boot gate.\n\n## watch\n\n`cotal watch` opens the read-only activity screen for the selected space. `w` is its shorthand.\nIt uses the existing observer view authority; a user-auth space requires the corresponding\nadmin view grant. It does not register a participant, bind an inbox or expose local process\ncontrols. Leaving with `q`, Esc or Ctrl-C stops only this observer.\n\nUse `--view events` or `--view messages` to select a feed, or press `f` to change it in the\nterminal. Arrow keys scroll; `g` returns to the latest observations. The feed retains at most\n300 entries. A reported turn ending is not task completion. An interrupted feed keeps its\nlast observations and reports the gap instead of showing them as current.\n\n`--plain` or a noninteractive invocation uses the attributed line stream. Interactive filters\nare not supported by that stream. Authenticated observer views keep direct messages out of\nthe channel tap. Opening this view never launches, stops or attaches an agent. If the reader\nof the stream closes it, as `| head` does, the observer stops the same way.\n\n### Named agent status\n\n`cotal status <agent>` reads the authorized manager census and separates Identity,\nExecution, Mesh and Last reported activity. JSON has `identity`, `execution`, `mesh`\nand `activity` fields; missing optional observations are null with source annotations.\n`--on <manager-instance>` restricts the read. It takes no `--all` or `--components`.\nA requested model is not provider-confirmed. Connector-reported provider\ninformation is labeled exact or name-matched under the same proof. A new manager\nrow marks a roster observation exact only when its principal and lifecycle UID\nmatch the managed seat and its presence view is current. Older or mismatched rows leave exact presence\nand activity null and label any name-matched values separately. A verified\ncondition and its reported start appear with activity; an unverified condition\nstays in a separate name-match field. A waiting observation is not a measured\nblocked duration, and a work event is not task completion. Attach and stop\nhints retain the answering manager's instance when it is a valid native id.\n\nA same-name match on several managers refuses as ambiguous. An unanswered manager leaves\nthe search partial; no matching row in that partial read is not proof the agent is gone.\nThe command starts, stops and attaches nothing.\n\n## Manager maintenance\n\n`cotal manager`, also `cotal mgr`, shows its two scoped operations. The group and its operations\nsupport `--json` and `--plain` through the shared reports. Neither operation installs connectors\nor starts a manager.\n\n`manager seats` lists retained local PTY custody records. `--drain --dry-run` reads the census\nwithout submitting a drain. A real drain reviews the local store and defaults to cancellation;\nscripts require `--drain --yes`. Its scope is all safely childless records in that store when\nthe native pass runs, not a frozen list from the review. Each native reap rechecks process\ncreation identity. Live children stay. An unresolved preflight blocks draining; a partial read\nreturns exit 4. An unconfirmed effect during a submitted drain returns exit 3 and keeps confirmed\nremovals in the result. Raw native diagnostic bodies are omitted.\n\n`manager reconcile` uses the existing guarded issuance-gate repair from the owning local mesh\nroot. It resolves the registered broker without an alternate-target fallback and preserves its\nTLS requirement across repair, liveness and eviction connections. It requires that root's auth\nmaterial and verified holder liveness. Review defaults to cancellation; scripts require `--yes`.\n`--dry-run` checks only the local coordinate and issues no credential or native guard query.\n\nA pre-effect native guard refusal returns exit 2. Its error names the guard's condition and keeps\nthe guard's own sentence as its native cause, which names the freeze-holder and the next step.\nA failure after the repair may have submitted\nwork returns exit 3, without replaying mutations or hiding possible revocation/eviction progress.\nRe-observe that gate and its operation-bound durable cursor before another explicit repair.\nThe final receipt reports the native operation and before/after coordinates, not a running-manager\nverdict. A reopened gate with retained cursor cleanup returns exit 4.\n\n\n### Workflow command forms\n\n`cotal run` (or `cotal r`) lists the authorized workflow records. `run show <run-id>` reads\nrecorded state, open requests and the last 20 journal rows. Its JSON result contains the full\nreturned status and journal. Revoked admission and unreadable revocation markers remain distinct\nfrom the record's state. Neither a record nor a receipt is a live agent-progress report.\n\n```bash\ncotal r\ncotal r start ./workflow.cotal.js\ncotal r show <run-id> --json\ncotal r resume <run-id>\ncotal r answer <run-id> '<step-key>' --value '<json>'\ncotal r amend <run-id> '<step-key>' --value '<json>'\ncotal r check <run-id> ./edited.cotal.js\ncotal r revoke <run-id> --local --by <actor> --reason '<text>'\n```\n\nStart takes a positional source file. Resume uses the host's recorded program. Hosted answers\nand amendments are attributed to the authenticated caller, never a supplied name. Check reads\nthe recorded journal through the existing local check; it commits no migration. Revocation keeps\nthe existing local trust and explicit actor/reason requirements and does not cancel a task.\nHosted start, resume, answer and amend have JSON and plain receipts. The local drive and local\ncheck retain their native output rather than claiming an unsupported structured format.\n\n```bash\ncotal r validate ./workflow.cotal.js\ncotal r plan ./workflow.cotal.js\ncotal r simulate ./workflow.cotal.js --handler ./results.json\n```\n\nThese three forms are offline and reject mesh or caller selection. Validation checks the\nlanguage. Plan lists validated static call sites in source order; it does not evaluate branches,\nloop counts or an execution schedule. Simulation uses the native simulator with a JSON results\nscript. It executes no JavaScript handler and invents no unscripted outcome. A simulation does\nnot establish live authority, installed applications or readiness. All three support JSON and\nplain output and contact no broker.\n\nInside a manager-launched static or open seat, `run turns` reads that seat's assigned turns and\n`run yield <turn-id> --status <done|blocked|handoff>` reports an outcome. Handoff also requires\n`--to <agent>`; `--note <text>` carries context. The native launcher supplies the caller context.\nThese forms verify the seat's own credential and manager, never substitute an operator caller,\nand refuse user-auth self impersonation. They skip operator catalog and installation repair.\nYielding one turn does not complete the entire workflow.\n\n\n### Update availability\n\n`cotal update --check` queries the latest registry version and reads recorded extension versions.\nIt performs no install, reconcile or process restart and takes no mesh selection. It supports\n`--json/-j` and `--plain`. The registry read runs npm with a temporary cache that is removed\nafterwards, no log files and no npm update notifier, so it leaves nothing in npm's cache, log\ndirectory or notifier stamp. Missing registry data or an unreadable extension inventory remains\nunknown or partial, never an empty successful check.\n\nThe report compares first-party recorded versions with the running CLI. It does not inspect\npackage contents, signatures or loaded process generations. `cotal update` and `update --self`\nkeep the existing guarded update, continuity checks, download verification and current-generation\nreconciliation. Their installer progress is native output, not a structured check receipt.\n\n\n### Account screens\n\nBare `login` chooses a known provider from cached account metadata or registered spaces. When\nnone is known, an interactive terminal asks for the provider auth URL. `--idp` selects it directly.\nThe device-approval link and code are written only to stderr. Session tokens are never printed.\nThe sign-in receipt separates account identity from each space's grants. This client's device\nflow requires approval and refuses `--no-input` before requesting a device session.\n\nBare `logout` chooses among cached providers. A single cached provider is unambiguous; scripts\nwith multiple providers must select one with `--idp`. `logout --json` and `--plain` report the\nsame result. No cached session is a local no-op, not proof that all provider sessions are gone.\nRevocation must return before local cleanup. If revocation is unconfirmed the login is retained\nfor retry; a later cleanup failure is partial and keeps the confirmed server outcome.\n\n\n`endpoints --json` and `endpoints --plain` show the authorized live presence snapshot, including\nagents and infrastructure. This legacy live read is labeled as presence, not a saved-reference\ninventory or proof that a service registration is active. Native contract resolution remains a\nseparate read.\n\n\n### Managed launch view\n\n`spawn [<persona>]` now uses the manager and attaches after its native readiness result. The\nattachment is pinned to the attributed accepting manager and the allocated identity/incarnation,\nnot a second display-name guess. `--detach/-d` returns a finite JSON/plain-capable receipt after\nthe same readiness result. The terminal receipt shows the full lifecycle, original request, accepted goal where reported,\nrole and model request separately from any provider acceptance claim. Its Next section names the actual agent, space and accepting manager\nin attach/status commands when their tokens are safe shell words; it otherwise retains\nplaceholders beside the full identity fields. Without usable input/output terminals, default attachment refuses\nbefore allocation and names detached or direct execution. A shell tool's inherited parent PTY\nis not enough.\n\n`--harness/-H`, `--model/-m`, `--name/-n`, `--prompt/-p` and `--cwd/-C` retain their declared\nmeanings. The legacy `--agent` spelling remains accepted; using both harness spellings refuses.\nA `--file <persona-file>` launch uses this route too unless `--foreground` is given. A manifest is\ndeployed with `cotal team deploy <file>`, not `spawn`.\n\n`--foreground` runs the native application directly. It does not support a structured receipt,\nmanager pin or manager control credential. Native exit or terminal loss may end that process;\nno detachable-view or reboot-survival claim applies. Protected internal handoff/enrollment\nchildren keep their existing direct execution and native authorization checks.\n\nIf the agent joins but attachment fails, the result is partial with its original identity and\nmanager. No second launch occurs. A view exit sends no stop request. This route does not offer\nno-manager startup choices or imperative planning.\n\n\n### Observer stream\n\n`watch --json` emits versioned watch NDJSON records from the same MeshView as the terminal.\nActivity, events and messages filters use the shared classifier. Metadata snapshots identify\nscope, reported roster state, native channel counts and membership-read failures. Feed rows keep\nnative message IDs and timestamps; coalesced updates are identified as observed rows, not new\nmessage publications or proof of task completion. No global sequence is invented.\n\nThe feed has a 300-entry window and bounded per-channel prefill, not a complete history export.\nWindow loss, connection transitions and text beyond the 16,384-code-unit row bound produce gap\nrecords. Identity fields are not clipped. The stream does not claim that arbitrary publisher\ncontent is secret-redacted. Leaving the observer stops its own connection and sends no agent\ncontrol request. Connection or authorization failure is an error, not an empty conversation.\n\n\n### Recorded team views\n\n`team` (`t`) reads fresh launch summaries, additive ownership ledgers and their\nroot-local attempt records at the selected local root. When native `up -f` returns from its broker start, it writes a creation-only fresh\nsummary with its full run ID, initial broker PID, declared agent/channel names\nand the initial control-plane, auth-service and delivery-responder results.\nOlder summaries without a broker PID, auth-mode pin or responder observation\nremain readable; `team show` labels their absent facts not recorded and\nreports partial. A PID\nor a responder bound when boot returned is not current process or listener\nownership proof. A private create-only fresh boot attempt is written and fsynced\nbefore the launch spec and broker start. A returned summary is also published create-only and\nfsynced; an intent without a matching auth-mode-pinned summary showing a\nreturned control plane, auth service and (for auth mode) bound delivery\nresponder stays unsettled and\nblocks a later fresh boot. `team show <id>` and the team inventory expose the original intent without\nclaiming whether its broker started. `team show` displays up to 100 declared names from a\nsummary and retains the complete list in JSON; none are claimed to have joined. A missing\nor refused summary after boot is not a reason to boot again.\nThe inventory stays partial because older fresh boots may have no summary. The\nterminal lists up to 100 records; JSON retains all readable records. Its Next column\nsays inspect the stack for a fresh record or review the native scoped teardown plan for an\nadditive ledger; neither row grants permission to stop anything. A fresh summary is\nnot an additive ownership ledger or evidence that an agent joined, and it does not establish\ncurrent process health. `team show <id>` identifies the recorded root and planned whole-stack\n`cotal down --space <recorded-space>` route for a fresh summary. The existing bare `down` does not use\nthe whole-set impact controller. This summary is not authority to run it.\n\nFor an additive record, `team show` prints up to 100 full recorded agent identities\nand lifecycle UIDs where the ledger has them, with the full validated ledger in JSON,\nwithout claiming those agents are still live. Its follow-up uses\n`down -f <file> --run <id> --dry-run` from the recorded root. The file is a required argument,\nbut the full run ID selects the ledger; an edited manifest is not new teardown authority. Review\nthe native plan before a separate apply. Listing ownership does not confer permission to stop\nanything. One guarded `team deploy <file>` route accepts a\nchannel-only manifest whose declared cards are all absent and explicitly set\n`deliveryClass: live`. It refuses agents, runtime requests, defaults,\nbroker bind changes, access edges, existing cards,\nre-apply, registered broker lists and unresolved prior attempts before an effect.\nIt selects the exact registered root, single broker address, auth mode and TLS\nrequirement, then holds the local maintenance lock. The original user bearer and its deployer and writer\nviews must name the same owner and actor. A refused view stops before the\nintent; no static or broader caller is substituted. A scoped writer uses one\nnon-reconnecting broker connection and KV create-only on the existing bucket,\nnever the legacy merge-and-put path. The private intent is published and synced\nbefore any create. Every channel has a synced submitted step before the native\nrequest and a synced accepted revision after its returned acknowledgement.\nThe returned receipt matches every declared channel and both steps. A missing\nacknowledgement or receipt leaves the attempt unsettled; no create is retried.\nIf closing the writer connection fails after the receipt is confirmed, the result\nkeeps the confirmed revisions and warns about the close. A returned receipt\nproves only historical created revisions, not the current\ncards, readers or removal authority. `team show <id>` presents one row per\nreturned channel; incomplete attempts show their submitted/accepted steps.\nThe JSON result retains both forms of evidence. Team inventory and status\nshow returned versus unsettled state. Additive broker addresses in reports\nomit URL user info, queries and fragments; the protected intent keeps the\nexact connection target. Invalid or orphan steps stay visible and\nblocking. Guarded channel-only card removal uses the returned create revisions as described below.\nCompletion reads those receipts locally; it does not query the broker or guess ownership from an\nedited manifest. Channel apply emits one finite rich, JSON or plain result.\n\nThe agent route accepts one to 100 all-new agents on a registered static-auth\nor open mesh, optionally with up to 100 new channel cards that explicitly use\nlive delivery. Defaults, bind changes and stale waivers refuse. User-auth agent\napply refuses before inline persistence. The route selects a manager through an attributed status read,\nthen pins credential issuance and every subsequent request to that full instance.\nIt requires native execution and a matching runtime; it never starts a replacement\nmanager. `--runtime` overrides the manifest's requested runtime but does not\ninstall or switch a provider.\n\nA mixed deployment also requires every requested application to appear available\nin that manager's connector inventory. This is a startup observation, not proof\nthat a later launch will succeed. A separate deployer reader checks the requested\nchannel keys, and a separately scoped writer uses one non-reconnecting connection\nfor native KV create-only. Manager credentials are not used as channel authority.\nExisting cards refuse; no patch, adoption or legacy merge-and-put occurs.\n\nOne private intent records all channels, ordered resolved agent hashes and the\nmanager before effects. Channels are created first, with submitted and accepted\nrevision steps synced before continuing. Each agent's original request is synced\nbefore submission, its native allocation before goal following, and its matched\njoined result before the next agent. A joined result must match the accepted\nname, principal, lifecycle, harness and runtime. Unreadable or unknown phases\nremain unsettled and are not replayed. The full receipt is saved only when both\nchannel and agent sequences match the original intent.\n\nA later failure retains earlier channel creates, allocations and joins without\nrollback. Writer-close uncertainty is reported separately from returned native\nreceipts. The finite JSON/plain/rich report distinguishes those receipts from\ncurrent health and task completion. `team show` exposes both native journals,\nincluding partial channel creates when a later launch did not complete. Inventory,\nstatus, down and reset distinguish returned mixed deployments from unsettled\nattempts. Retained verification and guarded removal use the paths below.\n\n`team boot <file> --dry-run` shows the resolved fresh topology, its target root,\nrequested applications, roles, models and channels with declared delivery,\nreplay and window settings. Absent channel settings are not inferred from a\nsaved registry that this preview does not read. An open manifest with an\nexplicit durable default or channel declaration is warned as unsupported:\nits native fresh apply refuses before effects because open mode starts no\ndelivery daemon. Direct `up -f --dry-run` refuses the same contradiction.\nIt reads the local control\ndirectory, recorded spaces, maintenance journal, the selected manager PID record,\npossibly live process records, the root's additive deployment ledgers and\nattempts, and fresh boot attempts/summaries, plus selected-space service\ndefinitions, instances, supervisor receipts and retained native attempts. An\nunreadable additive or fresh record is listed rather than treated as absent.\nThe team inventory separately lists unsettled fresh and additive attempts\nand returned agent/channel receipts. `team show <id>` distinguishes those\nhistorical records without inferring current card or broker state. Bare\n`down --dry-run`, `team deploy --dry-run` and both reset previews expose the\nrelevant retained additive IDs and unreadable paths. The legacy bare `down`,\n`down --preserve-state`, fresh `team boot` and `reset store` refuse unsettled\nintents and unremoved agent/channel receipts before effects. A complete matched\nstatic agent-retirement receipt releases only its original deployment's blocker.\nThis is a known-blocker guard,\nnot complete broker ownership. The service records belong to the workstation's selected-space store,\nnot necessarily this boot root; their recorded\nstate does not establish live ownership. A dead manager record remains visible,\nnot proof that a later boot may replace it. For a concrete candidate broker\naddress, the preview also lists other local registrations with that exact\naddress. A match blocks fresh apply; no match does not prove exclusivity against\naliases or remote tenants. A host-derived candidate leaves this comparison not\nchecked.\nWhere the manifest supplies a broker URL or uses the default without a host override,\nit makes a credless reachability probe. A failed probe is not proof of absence on a\nTLS-first listener. The result is partial: port binding, exclusive local ownership,\ninstalled providers, credentials, grants and startup readiness have not been checked.\nIt writes no record and starts nothing.\n\n`team boot <file>` requires a prepared local root and starts its environment through the service\ncontroller. A root without a stack journal takes the fresh static-auth or open route. An eligible\nstopped stack can resume in static-auth, open or user-auth mode. The manifest's channels seed the broker. The stack's own manager\nstarts its agents from a launch spec pinned by digest. One rich, JSON or plain receipt includes\nthe controller members and requested topology. Agent joins are not checked, so the receipt is\npartial. The fresh attempt and summary are launch records, not stop authority. No additive\nownership ledger is created; `cotal down` uses the stack journal and each member's native lifecycle.\n\nBefore writes it checks the selected root, auth mode, broker address, runtime, connectors and\nretained ownership records. A journal, legacy PID record, possibly live component, service instance,\nsupervisor receipt, native attempt, additive ledger or unsettled fresh attempt blocks a fresh boot.\nUnreadable records are never treated as absent. The lock protects local preparation, and ownership\nand bind checks run again before startup. A bind probe releases its socket and does not reserve\nthe address. A later failure keeps its attempt and any started members, reports the known partial\nor unknown outcome and never repeats effects. A failed summary write also keeps the running stack.\n\n`team boot --foreground` holds the stack's lifetime in the terminal. Ctrl-C or SIGTERM invokes the\nsame guarded whole-stack stop as bare `cotal down`, without force killing members. Terminal loss\nor an external kill is not a confirmed stop. Foreground apply takes neither JSON nor plain output;\na dry-run remains a finite preview. It does not establish native apply readiness or platform support.\n`up -f --foreground` still refuses on its retained launcher path.\n\nA recorded stack resumes only after its last whole-stack stop completed and its manager was never\nattempted. Its broker inputs must be recorded, every applicable member must have its original\nsettled stopped record, and no supervisor, retained custody or earlier team boot may remain. The\nbroker requires its kernel-exit receipt. Auth and delivery use their settled process stop and\nreleased custody; that is not proof of credential, lease or authority retirement. A generic\nprocess stop now records a `process-exit` receipt on the stopped instance when that stop signalled\nthe pinned process and saw it exit. `service stop --json` and `service status --json` expose it as\n`processStop`, including the stop request, original process identity, signal and observation time.\nHuman stop and status reports show the recorded signal, request and exit observation time, or\n`Process stop evidence: not recorded`. This is retained evidence, not a new signal from a repeated\nstop. An instance that was already gone gets `processStop: null`. Resume does not yet require this receipt. The resume receipt\nidentifies the prior stop and each member's evidence. Changed records refuse under the lock.\nA journal without recorded broker inputs cannot take this route. Restart it once with its original\nexisting `--store-dir` and its original `--max-file-store` and `--host`, if set, then run `cotal down`\nbefore the team boot. Omitting those values is safe only when the original start set neither.\nPreparation rechecks that explicit old store's directory identity instead of recreating it if it\ndisappears. These checks do not exclude an unrelated local process changing the path. Open broker\nconfiguration stays inside the selected root's `.cotal` directory, even when its store is elsewhere.\n\nFor a first user-auth team, prepare with `cotal up --user-auth --idp <url>`, then `cotal login`,\nexplicitly grant the invoking user with `cotal auth grant cli --sub <subject>`, and run `cotal down`\nbefore `cotal team boot <file>`. Do not start the manager between preparation and that stop. A\nstatic-auth or open preparation uses `cotal up --no-manager` before `cotal down`. No implicit owner\ngrant or borrowed caller is substituted. A previously attempted manager is never adopted.\nWindows cannot apply, because the stack's process services need Linux machine and boot identity.\n\n`--space` must agree with the manifest. Hand-registered and discovered spaces are not local boot\ntargets. Public broker addresses and fresh-attempt views omit URL user info, queries and fragments;\nthe private intent keeps the target. Native warnings and failures remain on stderr. Installed\nextensions may produce their own output outside the finite result.\n\n`team deploy <file> --dry-run` resolves the manifest and personas and requires a\nregistration for the declared space in this root. It shows the declared\nchannel delivery/replay/window values separately from its non-atomic live\nregistry classification. If the selected running mesh is open, an explicit\ndurable channel declaration is warned as unsupported by its missing delivery\ndaemon; the read-only preview remains available. Additive deploy does not\nseed manifest-wide defaults. For an existing\nunmanaged card, it names only explicitly declared fields that differ from the live value, never the live\ninstructions or description body. No declaration authorizes overwriting a\nsaved card.\nIt makes scoped, TLS-bound\nnative reads of the presence and channel-registry buckets. A complete presence\nscan can prove an empty roster; a failed or malformed scan refuses rather than\nclassifying absence. It classifies channel cards against the registry and agents\nagainst a unique prior ledger for the same manifest content. The derived membership\nfeed, when readable, shows a lower bound of other readers, not authorization.\nThese observations are non-atomic and do not reserve names or prove that a manager,\nruntime, connector, channel writer or launch grant will be ready. An unregistered or\nonly prepared space refuses. `--runtime` changes the requested runtime; `--allow-stale`\nis apply-only and refuses in preview. Apply accepts all-new live channel-only\nmanifests, or bounded all-new agent and mixed manifests on static/open meshes\nthrough one native manager. An identical settled channel-only deployment returns\na no-op only after its recorded target and every original create revision match\nleader-served native reads. It reuses that full deployment ID without issuing a\nchannel writer, creating an intent or changing cards. Missing or changed cards,\nambiguous or unsettled attempts, and removed ownership cannot become this no-op.\nNothing is repaired or adopted. The result names its observation time; it does\nnot establish current readers or health.\nFor an identical settled agent or mixed deployment, reapply verifies the original manager and every\noriginal incarnation. Static-auth uses active live slots; open meshes use the pinned manager's exact\nidentity and lifecycle rows. Mixed cards must retain their original create revisions and content.\nMissing, retired or replaced agents refuse; nothing is relaunched or adopted. Persona drift refuses\nunless `--allow-stale <names>` names the stale agents. Those agents keep their old policy and receive\na warning; the waiver never restarts or updates them. The no-op records an observation time and\nlabels its ordered reads non-atomic. Unsettled, ambiguous or removed ownership cannot become that\nno-op. User-auth agent launch and retained verification remain unsupported.\nRuntime requests must match the original incarnations; no runtime or manager replacement is inferred.\n\n`team remove <full-deployment-id> [--dry-run] [--yes]` can retire a returned\nstatic-auth agent-only deployment with at most 100 original incarnations. It\nrequires the original registered root, broker, TLS mode and exact manager.\nThe revision-32 native `slots` query checks each recorded name, principal and\nlifecycle UID. A successor, omitted slot or unconfirmed transition refuses\nbefore a stop. A dry-run only reads this impact; apply requires a default-cancel\nterminal confirmation or `--yes` in scripts.\n\nUnder the root maintenance lock, apply records one private synced removal intent\nbound to the original creation receipt. Each necessary stop has a synced original\nrequest marker and an expected principal/UID guard. An accepted stop is not\ncompletion. The CLI observes that same incarnation under a 30-second loop budget, in addition\nto each native read's request bound, checking\nits native retired slot, retired lifecycle head and cleanup-complete fact. It\nretains both revisions and the answering manager epoch before continuing. An\nalready retired incarnation needs no repeated stop. A missing row, successor,\ninterruption or observation failure stops the sequence without retrying effects.\n\nOnly a complete matching retirement sequence yields the create-only full removal\nreceipt. Earlier confirmed retirements and stop requests remain in rich/plain/JSON\noutput on a partial or unknown result. An unsettled removal intent stays blocking\nand is never resumed by repeating its effects. `team show` keeps its original creation and removal\njournals, and inventory/status/down/reset distinguish historical retired receipts.\nThe full receipt releases only this deployment's known additive blocker, not any\nother service, broker, shared-root or process guard. It never authorizes stopping\ncurrent alias holders. No channel, broker, service or remote account is removed.\n\n`team remove <full-deployment-id>` also removes the registry cards of a returned channel-only\ndeployment, with `--dry-run` for a read-only preview and `--yes` for scripted apply. It pins the\noriginal receipt, root, broker, auth mode and TLS requirement, and refuses changed card revisions,\nconflicting retained ownership or unreadable records. The preview discloses delivery and replay\nfallback to space defaults and removal of description/instructions. A derived membership feed is\ndisplay-only and never authorizes or blocks this metadata change.\n\nApply records a private intent before any delete. Each card delete is conditioned on its original\ncreate revision, then read back as a later removal marker. Already absent cards name the remover\nunknown. Missing acknowledgements remain unknown with a blocking intent and no retry. A complete\nreceipt releases only that deployment's retained blocker. History, grants and readers are unchanged;\nthe channel stays addressable and no reader exclusivity or complete cleanup is claimed. Repeating a\ncompleted removal returns its historical receipt without deleting a later same-name card. A new\ncard intent records the registry stream's name and creation time.\n\n`team remove <full-deployment-id>` on an unsettled removal intent reconciles it from native\nobservation instead of refusing. `--dry-run` shows each original member's classification, and\n`--yes` records only what the observation proves. It never sends a stop or delete, never repeats\nan unknown action and never continues a member that was not submitted; that member is reported\nunattempted and keeps the deployment blocking.\n\n- A card whose delete was submitted without a confirmed outcome is read with the deployer view\n from the registry stream the intent names, between two stream identity reads. A later removal\n marker, or no message at all, records `observed-removed` with the remover unknown, and it counts\n toward the receipt. A later value records `observed-superseded`; that card is not owned and the\n deployment stays blocking. A card still at its create revision stays unknown, because the\n original conditioned delete could still be applied. An older revision, a revision past the\n stream's last sequence or a recreated stream refuses. A read failure is never treated as absence.\n An intent written before stream binding keeps its submitted cards unknown.\n- An incarnation whose stop was submitted without a confirmed outcome is read with the privileged\n control instrument, which holds no stop or attach rows, from the original manager's exact-name slots. Only the original lifecycle\n UID's retired slot and head with cleanup complete record a retirement, under the original stop\n request. A terminal in progress stays open. A live original, an active slot with no live process,\n a retired slot without cleanup proof and an alias that now names a successor stay unknown. No\n process or presence absence settles anything. Open and user-auth deployments are unsupported.\n- A mixed deployment reconciles retirement first. Its card intent is observed only after a\n complete retirement receipt, and a retirement settled by reconciliation does not start a card\n phase.\n\nA receipt is written only when every original member settles as removed or retired. The\nreceipt then releases that deployment's blocker as an ordinary removal receipt does.\n\n`team remove <full-deployment-id> --dry-run` can also select a validated legacy ledger. It refuses a\nfresh boot ID or a changed root, broker, auth mode or TLS requirement. The old ledger removal path\ncannot adopt a new receipt. For a legacy ledger, a scoped deployer read asks the serving manager for a bounded native PS\nsnapshot and compares recorded IDs, names and lifecycle UIDs. It reads current\nKV revisions for ledger-named channels without exposing their descriptions or\ninstructions; an unavailable registry read refuses rather than treating cards\nas absent. The old ledger has no creation revisions, so a current revision and a\nmatching name do not authorize deletion. It also reads the derived membership\nfeed when available and shows other observed members on owned channels; that feed\nis display-only and cannot authorize channel removal. A missing or unusable manager\nPS refuses rather than calling owned agents absent. On a user-auth mesh a deployer view the\nidentity provider refuses is `PERMISSION_DENIED` (exit 2), with the provider's sentence as the\nremedy. An unavailable membership feed\nstays explicitly unknown. Other retained or unreadable additive attempts under\nthe root are shown separately; their effects are not attributed to this ledger.\nThese native reads are not atomic. The preview changes nothing and cannot\nauthorize a stop. Legacy removal still refuses effects, including with `--yes`. Agent retirement\nremains static-auth only. Channel-only metadata removal uses the existing scoped writer on static\nor user-auth meshes, or the open mesh's bare connection; it grants no new access.\n\nA returned static mixed deployment uses one preview, confirmation and maintenance lock. It first\nretires the original agent incarnations through their native manager. Only a complete retirement\nreceipt permits fresh card checks and deletes conditioned on the original creation revisions.\nBoth receipts are required to release the deployment's blocker. A partial or unknown phase stops\nthere without replaying its effects. An interrupted transition can continue only after complete\nretirement and before any card-removal intent exists. Open and user-auth mixed removal refuse.\n\n`team check <file>` resolves the native manifest and referenced personas without a broker.\nIt lists declared per-channel delivery class, replay and replay window without claiming an\nabsent field's live registry value; the native seed may merge over saved metadata.\n`--graph` shows effective channel access edges from merged agent policies, marking persona-inherited\nchannels that have no manifest card. It also shows each agent's role, capabilities and whether\ncapabilities came from the manifest, a persona or neither. Terminal tables show up to 100\nrows of each kind. The JSON result includes all resolved sources, merged policies,\ninherited scopes and, when requested, the channel graph, without persona bodies,\nkickoff prompts or opaque launch-option values.\nThis file check does not prove live authorization, installed applications or launch readiness.\nThese read views do not apply a boot, an additive deployment or a removal.\n\n\n### Terminal input receipts\n\n`input <agent> <text>` writes through the selected manager's native input command.\nUse `--text` when the value begins with a dash; do not combine duplicate positional and flag\nsources. `--no-enter` omits the trailing Enter request. JSON and plain receipts identify the\nanswering manager and its reported byte count without repeating the input body. An acknowledgment\nis not proof of application consumption or task completion. Unknown results are not retried.\nManaged-launch failures retain native request and accepted-goal identifiers when a reply supplied\nthem, including when following that accepted goal failed. No identifier is invented for a silent\nsubmission, and a missing effect outcome is not a proved pre-execution refusal.\n\nGoal observation keeps the caller's authorized route. A class caller reads its original goal on\nthat class route; an explicit `--on` or credential-bound instance stays pinned. Learning the\naccepting instance ID does not confer an instance route or cause another launch. The native\nfollower verifies the returned fact against the original caller and goal. A launch the manager\nrefuses before its first effect, such as a missing persona file or connector, carries\n`not-executed`. The CLI reports it `refused` with exit 2 and prints the manager's reason, which\nnames the file it looked for. A failure after the manager reserved the name or wrote a launch file\nkeeps its native outcome, and the CLI never reclassifies it from its message.\n\n\n### Stopping an agent\n\n`stop <agent>` requests graceful native despawn. `--force` requests the existing\n`graceful:false` contract; it does not grant authority or bypass incarnation checks.\n`stop --self` uses only a verified managed seat caller and its attributed manager. It accepts no\nother target or credential. Missing, mismatched and unsupported user-auth seat context refuses\nwithout switching to the operator. JSON and plain modes show the acknowledged native result,\nnot a separate measurement of process exit. Stop and input do not seed connector installations.\n\nFailed name resolution refuses before a stop, terminal input or first attach is submitted. The\nreceipt retains the inspect error and responding manager. It records local pre-submission proof\nwithout inventing a native effect outcome.\n\nWhen the manager refuses a stop, terminal input or first attach before anything ran, for example\nbecause the agent is not running or the caller may not control it, its reply carries\n`not-executed`. The CLI reports `refused` with exit 2, names the agent and prints the manager's\nreason on one line. A failure after an effect may have started, such as a terminal write the\nruntime rejected, stays `unknown` with exit 3, still shows the manager's reason, and is not retried.\n\nManaged imperative spawn accepts `--restarts <n> --restart-window <ms>` as a pair, using the\nexisting native `supervise` object. Both values must be positive safe integers. Direct foreground\nlaunch and the legacy manifest form reject these flags instead of ignoring them.\n\n\n### Native endpoint reports\n\n`describe <class>` shows the authorized command table, capabilities, target modes and answering\ninstance/epoch. JSON includes the native compiled schemas and verified closure digests. This is\nnative discovery, not a saved-reference inventory or a grant of authority.\n\n`invoke <class> <command> --args '<json>'` validates public inputs before target preparation,\nthen uses the native fetched contract before publication. `--on` pins one full instance ID; a\ncredential-bound pin must agree. Request IDs and actual replying instance/epoch remain in the\nresult. Refusals with a proved `not-executed` outcome stay distinct from unknown mutations.\nThere is no blind retry of an unknown operation.\n\nPlain and rich reports show at most 60 whole result lines; `--json` preserves the full returned\nvalue. Endpoint-provided result content is not claimed to be secret-redacted. A successful action\nacceptance is not whole-task completion. This route does not use saved references or cover the\ncomplete endpoint namespace.\n\n\n### Watch views\n\n`watch --view` accepts `feed`, `events`, `messages`, `roster`, `channels`, `waiting`, `dms` and\n`topology`; `activity` remains the feed spelling. The terminal cycles available views with `f`.\nRoster and waiting rows show reported state without inventing blocked duration. Channel counts\nkeep retained messages separate from observed arrivals. Topology displays broker-sourced live\npatterns and durable channels, not task dependencies or permission grants. Missing or unreadable\nmembership is explicit, never an empty complete graph. Long identities wrap without clipping.\n\nThe authenticated observer remains narrowed to channel traffic. Its DM view refuses instead of\nbroadening credentials or inventing a private conversation identity. Open-space DM observations\nretain native sender and target IDs. Use `--json` for versioned view records or `--plain` for line-oriented reports outside a\nterminal. Both use the same bounded observer lifecycle and shared view model.\n\n\n### Authority\n\nBare `auth` reports only safe cached provider metadata. It does not verify a session online,\nturn a sign-in into a grant, or print tokens. Its next steps name the `--space` you gave or the\nspace a bare command resolves; with no space they point to `cotal space`. `auth grants` reads the\nselected root's authoritative user-auth actor ledger; a root without its owner secret and pinned IdP\nrefuses rather than reporting an empty authoritative list. A static or open space has no such ledger\non any machine, so there `auth grants`, `auth grant` and `auth revoke` refuse with `UNSUPPORTED` and\nsay that actor grants apply only to a space started with `cotal up --user-auth`. A space that cannot\nbe resolved refuses with its next step, such as `cotal up` or `cotal space`.\n\n`auth grant <actor>` and `auth revoke <actor>` use the existing local operator authority path.\nChoose `--sub` or `--owner`, never both. Grant requires explicit scope/read/post lists unless\n`--full` deliberately selects the native wide defaults. These usage errors refuse with exit 1 before\nthe space or its authority is read, and one refusal names every missing input. Re-grant/revoke\nretain predecessor lifecycle retirement and concurrent-row checks before replacing or deleting\nauthority. A retirement that did not run, because the auth service is not running, refused the\nconnection or answered that the retirement did not run, is refused (exit 2) with the actor row\nunchanged and the recovery named, usually `cotal up`. Any other failed retirement request may have\nstarted the retirement, so it is unknown (exit 3) and is not retried. Grant\nrows in reports contain scope and ACL metadata, not token hashes or secret material.\n\nRevocation receipts separate committed deny-new from the delivery daemon's live-eviction notice.\nThey do not claim all related processes stopped. An interrupted mutation is unknown and is not\nretried. Neither grant management nor actor reads install connector packages. Grant management\ndoes not issue credentials, export signers, retire lifecycles or act on capability handles.\n\n\n### Saved endpoint references\n\n`endpoint` (`ep`) reads local references only and labels live state Not checked. `show <name>`\nshows the saved space, native class, full instance pin and storage revision. These commands do\nnot connect to a broker or infer registrations from participant presence.\n\n`endpoint add <name> --file <reference.json>` imports a strict version-1 reference object.\nGuided add is available in an interactive terminal. Names use case-safe hexadecimal\nstorage keys under the workstation endpoint directory. Imports are capped at 256 KiB. References\nreject unknown fields, raw inline credentials, target-space mismatches and malformed native pins.\nA credential reference is an owned private regular file; its contents are not read when saving.\nExisting names are not silently overwritten.\n\n`endpoint remove <name>` defaults to cancel, or takes `--yes` for scripted confirmation. It\ncompares the held revision and content under the private writer lock before forgetting metadata.\nIt never deregisters a native endpoint, stops a process, removes credential files or revokes grants.\n\nProtected no-follow/nonblocking file access supports POSIX hosts. Windows uses its access-list\nmodel for local records instead: a credential reference there must be a regular file, and its\nowner, mode and links are not checked. Saved references do not provide native live\ninventory, invocation resolution or the remaining endpoint verbs.\n\n\nSaved-reference reads validate record structure without opening or revalidating its credential\nfile. A moved credential does not prevent inspecting or forgetting the local mapping. Protection\nis checked when importing/saving a credential reference and must be checked again before any\nfuture invocation uses it. Read receipts label credential validation Not checked.\n\n### Local service definitions\n\n`service` (`svc`, `services`) reads the selected space's local definitions and existing component\nPID observations. Saved definitions, process presence, ownership and serving readiness are separate\nfacts. The definition table counts its already-read instance records and labels any sole instance's\nrecorded supervisor and historical readiness. The separate instance table gives full IDs,\nprocess observations and each recorded supervisor; a record count is not a count of live services.\nRetained native install, uninstall and unit-action attempts appear with their full instance ID,\nstep, position and whether an instance record still exists. They mark the inventory partial:\na submitted or unknown step may have changed the original unit even if its instance record\nis absent. A legacy PID record is not adopted into the new instance controller. Unreadable\ncomponent records make the inventory partial. Reads do not start, repair or install a service.\n\n`service add [name] --file <definition.json>` imports the version-1 service object described under\n[Adding a service](#adding-a-service). A declared preset uses\n`service add <name> --preset <preset> <declared-options>`. Guided input offers installed presets\nand file import. Input is capped at 256 KiB, checked against the selected native space and its\nschema before a private record is saved. Whether a root or credential suits the preset is checked\nat start. A process definition needs an existing executable and\nabsolute working directory. Credential file protection is checked without reading its contents.\nAdapter definitions use an installed local-process provider's versioned, closed option schema.\nAdding or previewing a definition validates cached metadata without importing provider code. The\ncache records SHA-256 fingerprints of the declaration and installed package metadata. Missing,\nchanged or version-skewed metadata refuses with an update or re-add remedy. These fingerprints\nare not publisher signatures or a proof of every executable file. No remote schema is fetched.\n\n`service show <name>` reads declared policy, not live state. Public reports omit argument bodies,\nadapter option values and credential contents. Missing launch paths do not prevent reading the\nsaved declaration. Records live under the workstation store's case-safe per-space service path.\n\n`service remove <name> --yes` forgets only the unchanged definition. Interactive removal defaults\nto keeping it. Unknown or foreign instance custody makes removal refuse. The process driver verifies that every\nowned direct incarnation is gone and that no OS supervisor is installed before removing its\nunchanged instance records and definition. Logs are retained. No process or OS unit is removed\nby this operation. The presets are described below. A preset is not installed under an OS\nsupervisor.\nOn Windows, local records use the access-list model described under `up`. There, `cotal ext add`\nand the setup seed record a service receipt for an extension that declares service adapters or\npresets, and starting one of its process services refuses Unsupported, because the process driver\nneeds Linux machine and boot identity.\n\n### Service presets\n\nBare `service` lists declarative presets separately from definitions and process observations.\nUse `service add <name> --preset <preset> --help` for that preset's option flags. Parsing, scoped\nhelp and completion derive those flags from the adapter's closed schema. Boolean options have\npositive and negative forms; a pair refuses. A boolean whose schema admits only true, such as an\nexplicit intent, has only the positive form. Numeric and complex options use JSON syntax.\nA preset no registered or installed provider declares refuses by name; for `web` the refusal\nnames `cotal ext add @cotal-ai/web`.\nOptions that collide with service/global flags require the file form. No preset is installed or\nprovider code imported by help, completion, a guided question or definition creation.\n\nA provider registers a `service-preset` alongside its `local-process` adapter. Preset metadata\ncontains its name, adapter, summary and readiness deadline. It does not duplicate option schemas,\ncontain executable factories or supply credentials. Cached preset and adapter declarations share\none package-metadata fingerprint receipt. A changed selected declaration refuses during dispatch.\nPath annotations require absolute physical files/directories, or owned private files, without\nreading their contents during definition validation.\n\nThe CLI's own presets are `auth-service`, `broker`, `delivery`, `exec-endpoint`, `feedback-intake`\nand `manager`. The `linear` and `web` presets come with their extensions. The intake preset is\n`feedback-intake`. Supply its `--root`, protected `--keys` and scoped\nbroker `--creds` paths in the selected registered space. Optional options include `--server`,\n`--host`, `--port`, `--channel`, `--store`, `--max-bytes` and `--rate-limit`. A supplied store must\nalready be private. Without one, the intake appends to a private instance-keyed store beside its\nservice records. Feedback records and logs remain private local data and are retained on removal.\n\nThe `broker` preset uses `nats-broker`. It requires a prepared local `root`, a protected existing\n`config` inside that root's `.cotal`, an absolute `binary` and one explicit `server` address. It\nprepares no trust, generates no configuration and starts no other service. Legacy broker custody,\nknown shared registrations, maintenance and mismatched transport refuse. Its creation-pinned\ndirect child writes a native ports report in a fresh private launch directory, and the readiness\ncheck sets that report to `0600` before it reads it. Its listener must\nmatch the declared IP, port and transport. DNS, wildcard and mapped-IPv6 target declarations refuse\nbefore launch. This reports the child's client listener, not JetStream readiness, endpoint serving\nor a fresh trust check.\n\nBroker stop has a separate kernel-exit policy, with no invented endpoint principal or epoch.\nKnown live or unresolved dependants refuse. After confirmation the controller rechecks the scope\nand process, sends one SIGTERM and waits for that incarnation to exit. Its receipt distinguishes\nprocess exit and removal of the ports file from a native shutdown acknowledgement or cleanup.\nAn unconfirmed stop keeps its original request; no force kill, replay or restart follows. A typed\nadapter refusal before child launch keeps its boundary code, safe cause and remedy. Its local\ninstance receipt remains diagnostic history, recorded as failed with no process, so a later start,\nstop, removal or `down` is not blocked by it. Named multi-service stops do not use this policy. Local registry absence does not prove that\nno remote client or tenant uses this broker, and whole-stack sharing is not checked.\n\nCreating the intake definition launches nothing. Starting it does not start a broker, manager, delivery\ndaemon or installer, mint a credential, or substitute an operator caller. The native child\nreuses the existing HTTP/mesh intake implementation and preserves the target's TLS requirement.\nIt publishes a create-only, full-instance/full-request startup receipt only after the HTTP listener\nand native signal handlers exist. Adapter readiness matches that receipt to the creation-pinned\nchild. It reports an owned running intake, not native endpoint serving, developer action or a\npromise that a future request will succeed. An old receipt is never a replacement launch.\nThe top-level `feedback-intake` entry is deprecated: it keeps its flags for existing callers and\nwarns on every run.\n\n`delivery` uses the selected registered broker root and an existing protected delivery credential\nfrom that root's native store. Supply `--root` and `--creds`; `--server` may repeat the recorded\nbroker but cannot replace it. Open targets refuse. The native daemon still checks its store,\naccount scan and singleton shard lease. No manager, broker or installer is started implicitly.\nA live, foreign or unresolved legacy delivery record refuses rather than being adopted. The\nadapter publishes its request-specific startup receipt after the native runner reports Ready\nand its shutdown handlers exist. That is not a fresh liveness response or proof of endpoint\nserving. The existing `deliver` machine-facing entry stays for the stack callers that still use it.\n\nNative service children retain only the explicit base/path/terminal environment allow-list and\nselected references. `COTAL_HOME` and XDG paths retain the invoking workstation registry. Arbitrary\nambient credentials and agent identity markers are not inherited by generic process services.\nA component needing the invoking caller must supply its supported native handoff, not infer a new\noperator identity. In particular, the web service does not receive the invoking caller's\nauthority.\n\n### Native Linear service\n\nThe `linear` service preset uses an existing native `account`, a reverse-DNS `endpoint` in a\nnamespace you own, and the selected registered static-auth `root`. An optional `server` only\nrepeats its broker. Account login/token setup remains a separate explicit operation. A start whose\naccount has no record refuses with `TARGET_NOT_FOUND` and the `linear account add` route before a\nchild is launched. That check reads the Cotal home of the process that runs `service start`. While\n`COTAL_HOME` is unset, a definition whose `envRefs` set `HOME` (on Windows, `LOCALAPPDATA` or\n`USERPROFILE`) can give the child another home, so that start refuses with `UNSUPPORTED` before a\nchild is launched. Set `COTAL_HOME` for the start, or remove that reference.\n`service add` checks only the declared option types and lengths. The account\nand the reverse-DNS endpoint name are checked when the service starts, and an invalid endpoint\nname refuses that start before a child is launched, leaving a failed instance record. The native\nstatic issuer, inventory, registration, credential renewal, request handling and deregistration\nstay in the Linear implementation. No generic caller authority, new account or broker dependency\nis supplied by the preset. User-auth and open spaces refuse instead of using another issuer.\n\nAn explicit start can allocate another instance through the native registration path. The preset\ndeclares foreground control through the shared terminal-bound lifetime; OS-supervised service\ncontrol is not supported. The child keeps the selected\nroot and registered TLS requirement and does not print upstream bodies or credentials into its\nstartup report. Its private phase records initialized native serve handlers and installed shutdown\nhandlers. It does not claim a broker-flushed subscription, a fresh authenticated serving response\nor upstream business-work completion. Reports label it process/adapter initialization, not\nendpoint-serving proof.\n\nStop retains the native serve close and bounded deregistration policy. Original process exit\nstill does not establish that deregistration or other cleanup completed. The legacy\n`linear serve` foreground entry remains available for installed callers; the independent\nservice route does not call that parser or its printed launch output.\n\n### Native auth service\n\nThe `auth-service` preset requires a registered hosting `root` with its existing prepared\nuser-auth projection. It loads the native callout account, issuer, owner secret and service key\nprojection. Saving or starting this preset does not generate those keys, create an account or\nstart its broker, delivery daemon or manager.\n\nThe preset options derive from the native daemon flags. Ports are bounded integers; public\nexchange options keep the native validation and reverse-proxy requirement. Advertised URLs\ncannot put credentials, query parameters or fragments on the child argv. No environment-export\nonboarding or generic caller-credential substitution is used.\n\nThe preset supports a prepared local user-auth root and its registered plain loopback NATS\nbroker only. The native authority path does not yet carry a required TLS policy through every\nconnection. TLS-required and remote broker targets refuse before authority startup rather than\nweakening that requirement. The preset declares foreground control through the shared\nterminal-bound lifetime; OS-service installation remains unsupported.\n\nThe real child exclusively claims the existing native per-space PID slot and publishes its\ncreation pin privately before opening authority planes. Any existing slot, including a dead or\nmalformed legacy record, refuses without adoption or reclaim. The startup phase is written only\nafter native discovery, flushed callout/listener initialization and shutdown handlers. Discovery's\nloopback capability stays in its native private store; no capability or bootstrap URL appears in\nthe startup phase. This is process/startup evidence, not a fresh authenticated endpoint response.\nA confirmed native close releases only this child's matching PID custody. An unconfirmed close\nkeeps that evidence. Service reports still do not claim lease, registration or family cleanup.\n\n### Manager stop impact\n\nThe native manager status contract includes its selected `lifecycleUid` and a caller-scoped `stopImpact` observation. It lists\nvisible managed principals and lifecycle IDs, their runtime modes and the seat actions a default\nstop or `--with-agents` would request. In-process PTYs without a release hook stop by default;\nreleasable seats lose manager custody. Preservation or pending resume uses the native retained-state\npolicy instead. None of these labels confirms seat exit, authority retirement or cleanup. From\nmanager cluster revision 33, a manager that is not `active` and holds a preservation inventory also\nreports `preservation`: the attempt and the inventory digest a preserved-exit stop names. A\npreserved-exit stop request carries that binding with `withAgents: false`; the manager accepts it\nonly when it is fully preserved, whole-manager and quiescent, and its result is\n`preserved-exit-returned`. The open-space local receipt keeps its revision-32 shape.\n\nThe manager resolves visibility through the same fresh owner/admin policy as `ps`. An\n`owner-filtered` observation is not the full manager impact, even if its rows happen to match a\ncount. A refused stop reports the selected manager, visibility, maintenance state and pending-work\ncounts that blocked its preflight, without exposing other owners' agent rows. Owner-filtered\nvisibility requires the stack operator or an authorized admin-scoped caller; the command never\nsubstitutes one. No stop policy is accepted while any blocker remains. This is\nan observation at the reported time, not a reservation, permission to stop or a guarantee that the\nset cannot change. Older managers can omit it; the native service adapter refuses when the\nrequired process/lifecycle correlation is absent. Generic process launch/stop still refuses managers.\n\nThe `manager` service preset uses the `manager-service` adapter. It requires the selected registered\n`root` and an explicit installed `runtime`; an optional `server` can repeat the registered broker.\nThe optional `console-port`, `console-host`, `ws-port` and `max-sessions` pass explicit native\nsettings. Omitting the host keeps the native loopback binding; port zero requests an allocated\nconsole port. A declared `launch` also requires `launch-sha256`. It names an existing private\nresolved launch spec, pins its full SHA-256 and must name this space. The child parses those\nverified bytes before authority preparation and keeps that snapshot through startup. An optional\n`roster` names an existing [roster file](define-a-team.md#roster-files) by absolute path. Only its\nowner or root may write it or its directory. The child parses it before startup, and each launch\nboots the agents it lists the way `supervise --roster` does. A resumed manager takes no roster.\nSaving these options does not prepare a manifest, provision an agent or prove that requested agents\njoined. Saving it starts nothing and grants no authority. Its native child starts no broker or\ndelivery daemon, does not adopt an existing legacy manager, and never prints its private console URL.\nIt declares one instance per definition and runs in the background or with `start --foreground`.\nIt is not OS-supervised installation or platform parity.\n\nThe real child exclusively claims its creation-pinned native PID custody before hosted authority\npreparation or local endpoint initialization. That starting record is not readiness; another existing slot is not adopted or reclaimed.\nAfter a crash, the record the killed child left names a dead pid. The manager adapter removes that\nrecord before the next launch, so the next `up` relaunches the manager; a live, foreign or\nunreadable record still refuses.\nIt binds its actual native identity before endpoint construction. A startup phase appears only\nafter native start, settled custody and signal handlers. On authenticated meshes the adapter also\nrequires a current status response pinned to that native instance and epoch, with the same process\nlifecycle UID. It preserves the invoking managed-seat caller, registered root/broker and required\nTLS. Missing caller support refuses without an operator substitution. The completed status read's\nown admission is excluded from stop impact; other admitted work remains counted.\n\nAn open mesh uses the pinned child's private request-bound local observation. Its service, launch,\nrequest and both process creation identities must match, and its registration must match the\nlaunch identity. Output labels this a child-local receipt, not a broker-attributed endpoint\nresponse. A retained stop handoff blocks readiness adoption on either route instead of treating\na live PID as a new serving launch.\n\nA real `service stop` or `restart` reads complete caller-visible impact before confirmation.\n`--with-agents` requests native agent stops, not generic descendant signalling. An interactive\nconfirmation defaults to cancel and shows every affected identity, lifecycle and requested seat\naction. Scripts require `--yes`. The offline `--dry-run` still imports no provider and performs no\nnative impact query; it is not stop authorization.\n\nThe native stop reserves its original PID custody without reclaiming an existing reservation.\nIt rechecks the confirmed scope, binds the full service/launch/stop request and both process creation\nidentities, and uses the existing native shutdown intent for agent reap. The child rechecks the\nsame complete scope before accepting. Changed or incomplete scope refuses without a broader policy.\nThe controller requires immutable accepted/returned evidence and the original process exit before\nrecording a native stopped state. An unconfirmed request retains its reservation and never escalates\nto force kill, restarts, or treats PID disappearance as completion. A later bare `up` or `down`\nsettles it only from the same evidence, and only then releases the reservation. Reported cleanup remains not\nchecked. Retained native-policy markers prevent a metadata change from selecting generic signals,\nand an unconfirmed native stop cannot be forgotten just because its process disappeared. Legacy shutdown refuses the native child rather than bypassing that boundary.\n\n### Resolving endpoint targets\n\n`endpoint describe <target>` and `endpoint invoke <target> <command> [--args '<json>']` use the\nexisting native caller and digest-verified contracts. `ref:<name>` requires a saved reference.\n`ep:<class>` bypasses local references. An unprefixed name checks the selected space's saved\nreferences first, then uses the native class only when no reference exists. An unreadable or\ninvalid reference refuses instead of selecting another target.\n\nA selected reference is disclosed on stderr and in `result.resolution`, with its revision and\nresolved native class, instance and epoch. The CLI holds its cooperative writer lock and checks\nthe same bytes before invocation. `--on` must agree with a saved instance pin. A missing pinned\ninstance does not fall back to the class queue. Unknown native effects are not blindly retried.\n\nA reference's file credential uses the [paired native caller adapter](#endpoint-caller-credentials).\nIt must match the selected registered static root, space, endpoint and signed native scope. A\nconflicting explicit `--creds` refuses. Arbitrary raw files do not supply a caller triple, and a\nreference's instance pin cannot be dropped to fit a class-only file. Neither case substitutes an\noperator identity. References without a file use the normal registered-space caller and its\nexisting capability checks. No local reference grants remote authority.\n\n### Declared capabilities that remain gated\n\nThe CLI grammar also names `endpoint watch <target> <record-key-or-topic> [--since <native-sequence>]`,\n`endpoint session <target> <command>`, `endpoint work lease <target> <pool>` and\n`endpoint work commit <target> <pool> --lease <private-file> --result <JSON>`. They return a typed\n`UNSUPPORTED` result before any broker connection, subscription, cursor, session, lease or commit.\nThe current resolver supplies command contracts, not a verified mapping of these application\ncapabilities. This is a CLI integration limit, not a claim that the selected endpoint lacks them.\nNo arbitrary subject, pool implementation or remote execution is guessed.\n\n`endpoint invoke --handle <private-file>` is also an explicit unsupported boundary, because no\nverified native auth-slot adapter is connected. It opens no file, redeems no handle and publishes no call.\nAn opaque low-level auth string alone does not establish receiver-side attenuation. Offline\n`auth handle inspect` remains explicitly unverified metadata without `--space`; its online\nstatic-registry signature inspection also does not establish presenter binding or use authority,\nand is not a substitute for that gate.\n\n`endpoint describe --refresh` refuses as unsupported and sends nothing. This CLI keeps no resolved\ncontract between calls: each describe makes a fresh describe request and fetches the contracts\nagain, so there is no cache for the flag to bypass.\n\nA saved-reference or paired-file invocation uses the described instance and epoch without bind-repair\nto a different handle. A pre-effect bind refusal remains a refusal, not permission to choose another\ninstance. Invocation receipts attribute resolution to the native responder that answered.\n\n`endpoint --live` adds authorized native service-registration metadata to the local reference\ninventory. It uses the existing scoped service-record read rows, without adding grants or reading\nmessage bodies. The SDK paginates the filtered metadata list; per-key spec/status reads come from\nthe leader. The list can lag concurrent changes and is not an atomic whole-space snapshot.\n\nThe snapshot reads at most 1000 registration keys. Omitted keys, unreadable records or unavailable\nregistry access make the requested live portion partial and return exit 4, while preserving the\nlocal references. Status projection, reported state, process ownership and a serving response are\nseparate. No active probe or process control is implied by a registration row.\n\n### Clearing retained history\n\n`history clear` purges all retained channel history in the selected space. It takes no channel\nargument. `--dms` also requests DM history under the existing purger authority. In a terminal,\nconfirmation names the space, broker and space-wide scope and defaults to cancellation. Scripts\nand `--json` require `--yes`. The old `--force` spelling refuses with the replacement.\n\n`history clear --dry-run` resolves the target and names the affected streams offline. It issues no\ncaller credential, connects nowhere and does not check authorization or retained message counts.\nA space that is prepared but not started is refused with `TARGET_UNAVAILABLE` (exit 2), and the\nrefusal says how to start it.\nThe purge uses the existing purger instrument or one-action user purger view, not a broader caller.\nOn a user-auth mesh whose grant lacks scope `admin`, the purger view is not issued and the refusal\nis `PERMISSION_DENIED` (exit 2). Its remedy is the identity provider's own sentence, which names\nthe missing scope and the whole-row re-grant that adds it.\n\nJSON and plain receipts retain each native stream acknowledgement and message count. A later\nrefusal after an acknowledged purge is partial. An unconfirmed submitted purge is unknown and is\nnot retried. Clearing history does not stop agents, remove stream definitions or reset identity;\nnew messages may arrive after the acknowledged purge.\n\n### Following an endpoint action\n\n`endpoint invoke <target> <command> --wait` requires the command's digest-verified action marker\nand the native goal-result read surface. The CLI submits once, retains the accepted goal ID and\nfingerprint and follows through the native helper. Reconciliation reads the accepting instance,\nnot an unrelated class winner. A reference keeps its described binding for submission.\n\n`--wait --json` emits goal NDJSON. It exports the original request, acceptance and observed terminal\nresult. Intermediate native progress is not exported; a gap record states that limitation. No\nglobal sequence or task progress estimate is invented. An unknown observation keeps its accepted\nidentifiers and is not retried. Ctrl-C or termination ends observation, not the underlying action.\n\nGoal-stream terminal values are capped at 16384 UTF-8 bytes. A larger value is omitted with an\nexplicit gap and retained goal identity, not split into invalid JSON. Read that original goal's\ncanonical result if needed; do not resubmit an action to recover its output.\n\n### Unconfirmed requests\n\nClassified failures use the same report layout as successful commands. The report names the\noutcome and stable code, then shows available request, goal and native responder attribution.\nUnknown and partial results use waiting treatment, not a healthy completion marker. Plain mode\nkeeps the same state words without ANSI. The JSON envelope stays on stdout; diagnostics stay on\nstderr. Raw argument objects and arbitrary native detail payloads are not copied into the human\nreport. Confirmed history purge acknowledgements remain visible when a later stream fails.\n\n### Persona previews\n\n`spawn --file/-f <persona-file>` selects a persona, not a team manifest. It cannot be combined\nwith a positional persona or the persisted internal `--config` spelling. A bare filename is\nstill a file, including when it has no extension. Managed paths remain relative to the manager's\nworkspace and are read on that execution host. No file upload is implied. Direct foreground\nexecution reads its local file. A supported Mesh-manifest shape returns a schema refusal with\nthe literal `cotal team deploy <file>` remedy, before agent allocation.\n\n`spawn --dry-run` never refreshes the catalog, repairs stale registrations, imports connector code,\nconnects to the broker, redeems enrollment, provisions or allocates an agent. The managed preview\nis currently partial: it retains requested selectors but cannot resolve host-local persona and\ncwd facts offline. It does not substitute the caller's files. With `--foreground`, the preview\nresolves the local persona, harness precedence and physical working directory. Binary, login,\nmodel-policy and live authority checks are labeled not performed.\n\n`spawn --foreground --enrollment-file <private-file> --space <name>` uses the existing one-seat\nforeground enrollment path. The file is opened with protected ownership/mode and bounded input\nchecks; its URL is not printed, copied into the child environment or persisted by this input\nadapter. It does not supply a managed enrollment authority path. Dry-run does not read or redeem\nthe enrollment file. On Windows the file must be a regular file; its owner, mode and links are not\nchecked.\n\n### Owned process services\n\nPreset inventory checks the selected installed declaration without importing provider code. It\nshows declared foreground/multi-instance capabilities, process versus endpoint readiness, and\nnative-request-bound versus direct-process stop policy. Missing, skewed or unreadable metadata\nleaves the inventory partial, with unknown capability fields. Passing metadata checks is not\nproof that an application, dependency or service is ready. Linux direct-process\ninstances can use the owned systemd user-unit route described below. An adapter\ndoes not acquire that mechanism from its name or preset.\n\nThe direct-process driver pins Linux machine/boot IDs and process start ticks.\nmacOS and Windows still refuse: their generic process custody needs a stable\nmachine, boot, owner and subsecond creation pin before any signal is safe.\nThe existing macOS `ps -o lstart` timestamp has only second precision, so\nhashing it would not distinguish a PID reused within that second.\n\nInstance reports separate recorded supervisor policy from supervisor discovery, which remains not\nchecked by this direct-process driver. A recorded `none` means no supervisor assigned by this\ncontroller. It is not proof that an external unit cannot supervise or restart the process.\nA retained supervisor receipt binds its definition revision, full instance, machine and OS user.\nIts file view hashes only the recorded physical unit, without printing its body or environment.\nMatching bytes are not loaded/active state or enabled-autostart proof. Missing, modified or\nunreadable unit files do not turn a receipt into an uninstalled service. Direct start/stop/remove\nrefuse while such a receipt is retained; they do not launch beside a unit or implicitly uninstall\nit. `service status` can read an unchanged recorded systemd user unit on the same host through\none bounded exact-unit query. It checks the returned physical fragment and reports native state,\nmain-PID attribution, invocation, reload need and presence of unverified drop-ins. Missing or\nambiguous observations stay partial. It neither prints unit/environment contents nor discovers\nownership from names or cwd. This observation proves neither endpoint readiness nor effective\nlogin/reboot autostart. Inventory and direct-process dry-runs perform no OS query;\nLinux native-unit removal dry-runs make one bounded exact-unit status read.\nOn Linux, `service install <name> --instance <full-id> --dry-run` asks the\nsystemd user manager for its config directory, then renders a\nbounded direct-process unit and private environment candidates and reports\ntheir paths and digests without printing their bytes or the inherited `PATH`.\nWith no existing instance, the inspection checks the declared process,\ndurable CLI entry and user manager, then reports that apply would allocate a\nstopped full ID; it\ninvents neither an ID nor a unit candidate. On macOS the same preview renders\na bounded LaunchAgent plist candidate for a settled stopped direct-process\ninstance, with its label, path and digest but not its body or inherited `PATH`.\nIt checks the `Library/LaunchAgents` segments without following symlinks;\nit does not query a launchd domain, load a job or enable autostart. An existing\nplist remains unattributed. With no instance it reports a possible stopped-ID\nallocation, not a plist. macOS install apply refuses, because it has no native\ncontroller or custody records.\nLinux bare install allocates that stopped full ID only after its source validation;\nit does not start a process. If later installation fails, the allocation is\nreported as a changed resource rather than hidden or retried.\nThe unit environment pins an absolute-entry `PATH` from this invocation;\nrelative or empty entries refuse rather than acquiring another working\ndirectory at boot. It requires a settled stopped instance, its current\ndefinition revision and no recorded supervisor, host environment reference or\ncredential reference. An existing unit at that path remains unattributed, not\nowned. The directory's parent ownership, private seed, receipt intent, loaded\nunit and autostart are not established; the preview is partial and authorizes\nno install. Linux apply retains a per-step intent before exclusive private file\nwrites, isolated connector seeding, a bounded private-tree fingerprint, user\nmanager reload, enable without `--now`, and the exact supervisor receipt. It\nreturns the full instance and observed unit-file state, not a process-readiness\nor effective reboot-autostart claim.\nA retained installation or removal intent blocks a new install and is reported\nas partial by `service status` with its exact unit path, private-store path,\ncurrent step and last acknowledged step. A bounded native read for each retained install, removal or start/stop/reseed\naction distinguishes an unchanged physical unit fragment from a name-only\nstatus when the file is missing or modified. It also classifies the private\ndirectory without inspecting its children. None of these reads permits an\neffect to resume. It cannot\nauthorize a blind retry. A changed\npre-effect target leaves a planned step and a partial receipt; an unconfirmed\nsubmitted effect is unknown and is never automatically repeated.\n`service remove` refuses while any native attempt names the definition. There is no\nexplicit reconciliation for an uncertain attempt.\n`service uninstall <name>\n--instance <full-id> --dry-run` reads only a matching recorded systemd unit,\nits private-store directory and one native status snapshot. A retained native\naction, or missing, modified or foreign receipt files, refuses. An owned private\ndirectory does not prove its descendants unchanged. A separate unexposed\nremoval candidate requires a bounded no-follow fingerprint of the complete\nprivate tree to match the install receipt. Changed content is preserved and\nremoval refuses. The preview does not reserve a stopped unit, enable state or\nremoval authority; its report remains partial. Linux apply requires the exact\nunchanged loaded inactive unit and private tree, with `--yes` in scripts or a\nscoped terminal confirmation. It disables autostart, removes only the pinned\nunit and private tree, reloads the user manager, then removes the supervisor\nreceipt. The definition and stopped instance record remain.\n\nA retained native start, stop or reseed action is reported as partial by\n`service status`; it does not authorize another request. Native start and stop\nrequire the exact owned receipt and current native unit observation. An older\nprivate store is reseeded while the unit is stopped, with a separate retained\naction and refreshed generation/tree digest before start. An unconfirmed seed\nis never retried. Restart uses the same exact stop and subsequent start; if\nstart does not confirm, the stopped original remains in its result.\nUninstall retains a step before disable, exact unit unlink, reload, private\ntree removal and receipt deletion. A non-confirming step remains visible in\n`service status`; a missing unit alone never clears its attempt.\n\nThe private unit entry resolves only its built-in command and validates the explicit controller\nhome, space, definition and full instance before any extension overlay, repair or package load.\nIt refuses an inherited managed-spawn handoff without reading or deleting that file.\nBefore native replacement, it requires the recorded private seed generation,\nexecutable provenance and absence of unfinished seed markers; it never reseeds\nthat store at boot. For a\nrecorded direct-process unit it also requires no retained installation or\nremoval intent, the current native unit invocation and matching physical\nfragment before replacing itself with the saved executable. It does not fork a detached\nwrapper or retry execution. A starting record does not establish successful replacement or\nreadiness; an interrupted replacement remains unknown. A later unit invocation refuses an\nunsettled starting, stopping or unknown attempt instead of replacing its request.\nInstall dry-run makes only the bounded native directory read above; uninstall\ndry-run reads the exact recorded unit and private-store directory. Neither\nallocates an instance, enables autostart nor removes a receipt or unit.\n\nProcess definitions support `service start/stop/restart/status/logs <name>`. The driver currently\nrequires Linux stable machine identity, boot identity and readable process-creation tokens. It\nrefuses another platform instead of using PID-only ownership. It launches executable and argv\nseparately without a shell. No broker, manager or other dependency is started implicitly.\n\nInstance IDs are assigned once and printed in full. With several records, scripts need\n`--instance <full-id>`; a terminal can choose from those IDs. `start --new-instance` explicitly\nrequests another process-driver instance. Otherwise one serving instance is a no-op and an\nunresolved prior launch blocks another allocation. A launch that failed before its child existed\nis recorded as failed, not unresolved. A stopped instance retains its logical ID\nand receives a new launch request. Unknown startup retains its receipt and is not resubmitted.\n\nOnly safe base environment names and the definition's selected `envRefs` are passed. A declared\nprotected credential file is forwarded to that program as the `COTAL_CREDS` path, not credential\nbytes. Control credentials are not inherited wholesale. The program still runs as the current\nOS user; this is not a process sandbox. Native managers require their service adapter and\nagent-impact guards and are refused by generic process launch/stop.\n\nAdapter definitions use the same owned direct-child controller. The local-process provider\nadds a service ABI declaration and launch/readiness hooks. Its closed option schema supports\nbounded objects, arrays and scalar fields, with no schema references, regular expressions,\ncoercion, defaults or unknown-option removal. Every object closes its declared fields. An\nunsupported schema refuses rather than being widened. A declared path option and its ancestors\nfollow the [`space create`](#selecting-a-space) rule for other local writers; a private file stays\nowner-only.\n\nThe provider declares foreground, multi-instance and log capabilities. An explicit start records\nits definition revision, full instance, launch request and adapter package/version/declaration\nfingerprint before loading provider code. The launch hook receives the definition's `envRefs`\nnames, never their values, and returns executable, argv and working directory separately. It must not launch children, mint credentials or start dependencies while\nplanning. A changed adapter is not adopted by a retained instance. Native stop declarations add\nclosed impact and terminal hooks; a missing hook refuses instead of using generic signals. The\nmanager's adapter supplies its own native policy and receipts. A preset is not installed under an\nOS supervisor.\n\nReadiness hooks receive the pinned process and a bounded cancellation signal. Ready observations\nmust match that process and carry a current observation time. An endpoint-serving declaration\nalso returns its checked native class, instance, principal, lifecycle and epoch. The adapter must\ncorrelate those native coordinates with its owned launch using its supported authority; a matching\nname or successful HTTP request is insufficient. Reports preserve those coordinates and their\nobservation time. Local status does not refresh them or promote them to current live authorization.\nA failed or timed-out hook retains an unknown receipt and is not retried. A cancellation signal\ndoes not prove third-party code stopped working.\n\nExtension removal checks retained service records in every local service space, including spaces\nabsent from the mesh-selection registry. Live, foreign, unreadable or unresolved instances block\nremoval. A starting intent and the extension writer lock prevent an uninstall from crossing the\ncontroller's final provider check. Providers still run as the current OS user, not in a sandbox.\n\nProcess readiness reports a live creation-pinned direct process as Running. HTTP readiness uses\na bounded loopback request with credentials omitted and redirects disabled. Its success is\nReachable, not proof that the child owns that listener or serves a native endpoint. Status labels\nthe stored readiness observation and its time separately from the current process observation.\n\nStop and restart default to cancellation in a terminal and need `--yes` in scripts. The selected\nrecord revision and creation identity are checked before signals, including escalation. A reused\nPID is never signalled as the old process. Restart holds the definition operation across the\nconfirmed old-process exit and one replacement launch. An unconfirmed stop cannot cause a\nreplacement. `--dry-run` creates no instance and sends no process signal.\n\n`start --foreground` is an owned foreground process, not an attachable view. It rejects JSON and\ncannot adopt an already running background instance into the terminal. Before allocation it prints\nthe terminal lifetime and output policy. Native auth and Linear presets use their installed signal\nhandlers through this same flow; neither endpoint readiness nor native cleanup is inferred from\nprocess exit. A foreground manager stops through the same request-bound native stop as\n`service stop`, keeping its agents: the SIGINT or SIGTERM the CLI receives is the confirmation.\nCtrl-C also reaches the native child, which ignores a signal that carries no stop request and\nprints one warning line for it.\nNative service child boot never repairs or seeds connector packages. Background stdout/stderr\ngo to owner-private logs. `logs --tail <1..1000>` reads a bounded tail; omitted earlier bytes make\nthe result partial. Foreground output is not captured there. Log text is not promised to be\nsecret-redacted. Live following uses the bounded read-only stream described below.\n\nFor a recorded systemd user unit, logs use its saved original invocation, boot and OS user,\nnot the native manager's current invocation. Finite tails read at most 1 MiB and 1000 records.\nIndividual native JSON records are capped at 256 KiB. Native journal cursors and microsecond\nwall/monotonic timestamps are kept; binary or repeated message values stay native JSON. Missing\ninvocation evidence refuses without substituting an old direct-child file. Native diagnostics are\nwithheld, not suppressed by the query; their presence makes a finite result partial.\n\nJournal following freezes those same selectors. It reports oversized, incomplete or unattributed\nrecords as gaps and emits native cursors, rather than inventing a sequence. Ctrl-C closes only the\nowned journal reader. It never signals the service or adopts a later invocation. This scope includes\nreadable process-origin messages, not every user-manager lifecycle diagnostic. Journal access and\nretention may omit messages, and an empty result does not prove there was no output or work.\nJournald must retain an accessible per-user journal; no alternate file, privilege escalation or\nremote log service is used. These are authorized local logs and can contain secrets.\n\nThe process driver tracks its direct child, not arbitrary descendants a program creates. A\nstopped/direct-process-gone observation must not be interpreted as proof that all external work\nor descendant processes ended. The process driver itself provides no service presets, agent-aware\nmanager control, owned OS units, non-Linux lifecycle parity or full preset and OS-supervisor\nintegration.\n\n`service logs <name> --instance <id> --follow` follows that private file without controlling the\nservice. JSON emits log snapshot/event/connection/gap/error/end records. Events carry native file\nidentity and byte offsets, not a manufactured global sequence. Initial tails and reader backlog\nare bounded at 1 MiB and chunks at 16384 bytes, with explicit gaps for omissions, replacement or\ntruncation. Instance-generation changes are attributed. UTF-8 decoding can replace invalid bytes\nand is labeled accordingly. Output backpressure pauses the reader instead of growing a buffer\nwithout bounds. Ctrl-C, termination and closed output end the view, not the service.\n\n`auth retire <full-lifecycle-uid> [--space <space>] [--yes]` validates the native UID and resolves\nthe selected auth target. The CLI then refuses unsupported caller context: the registered\nauth endpoint requires the requester's own current serve identity/gate, which the standalone CLI\ndoes not provide. It neither borrows a manager identity nor substitutes the interactive/managed\nloopback host doors. `--yes` cannot create authority, and no retirement request is sent.\n\n`auth redeem <private-handle-file> [--space <space>]` selects the existing local auth target and\nrefuses with `UNSUPPORTED` because its conferring-handle redemption path is not exposed. It does\nnot read the file, consume a handle or issue credentials. Session grants, agent enrollment and\nparsing an unverified handle are not substituted for that authority.\n\nNested help follows declared command words, for example `cotal help auth handle inspect`.\nAliases resolve within their declared parent. Help reads cached metadata and imports no provider,\nstarts no service and opens no credential file. A command's `-h` selects the deepest declared\nmode before its data arguments; it does not execute that mode. Scoped options refer to the\ncommand's shared flag definitions. Completion uses the same declared subgroups and option names,\nwithout treating an option's value as another command word.\n\n\n### Endpoint class scatters\n\n`endpoint invoke <target> <command> --all` submits one native ephemeral class scatter.\nIt freezes the expected live instance set, gathers attributed replies and reconciles registration\ncurrency through the native SDK. It does not probe additional instance rails or retry a mutation.\nInstance pins, targeted commands, `--cast` and `--wait` cannot combine with a class scatter.\nThe installed helper does not submit journaled commands through this route.\n\nFinite JSON retains the original request, each valid member result and the native missing, churn,\nunexpected, duplicate, late and invalid buckets. Invalid-reply diagnostic bodies are omitted.\nCoverage and command success are separate: complete coverage can still contain refusals or failures.\nAll confirmed pre-execution refusals exit 2 (invalid input exits 1), all executed failures exit 5,\nunconfirmed execution exits 3 and mixed or partial outcomes exit 4. Reports keep full IDs, show\nbounded member/observation rows and state their omissions. A missing reply is not proof of death.\nResult values are endpoint-provided and are not guaranteed to be secret-redacted.\n\n\n### Personas on an exact manager\n\n`persona --on <full-manager-instance>` lists that manager's authorized catalog.\n`persona show <name> --on <id>` reads a card only when its native ownership policy allows it.\nA name-only row does not grant access to its instructions or permission to redefine it.\nThese operations never substitute the local catalog or another manager.\n\n`persona create <name> --on <id>` accepts one of `--file`, `--prompt` or `--from`, with\n`--from -` as explicit piped input. Leading frontmatter follows the native persona merge:\nthe supplied name wins, explicit model/role/application and channel fields override the file,\nand the manager preserves existing ownership and redefinition policy. Inputs are bounded to\n1 MiB and must be valid UTF-8. A malformed leading block refuses before the remote request.\n\nThe remote wire accepts name, body, model, role, application, initial read channels and read/post\nACLs. `--harness`, `--subscribe`, `--allow-subscribe` and `--allow-publish` name those fields.\nOther frontmatter, including kind, tags, description, attention settings, variant, launch options,\ncapabilities and owner, refuses as unsupported instead of being silently discarded. Remote edit\nand removal are not native wire operations and refuse; no local file is changed instead.\n\nCreation is silent unless `--announce <concrete-channel>` is supplied. Announcement preparation\nmust resolve to the same space, root and account context before the write. A confirmed save and\na later denied or unconfirmed announcement produce a partial receipt retaining the save. Do not\nrepeat the definition to recover an announcement; read the channel first. A stored announcement\nis not delivery, a spawned agent or task completion. Finite JSON/plain receipts keep the requested\nand answering manager, epoch and original request, without printing the creation input body. Rich results offer\nfollow-up show or spawn commands with the original exact manager pin; a suggested launch still\nrequires the caller's native spawn authority.\n"
|
|
65277
65277
|
},
|
|
65278
65278
|
{
|
|
65279
65279
|
"slug": "config",
|
|
@@ -65441,7 +65441,7 @@ function loadDocsBundle() {
|
|
|
65441
65441
|
"title": "Setup internals (maintainer notes)",
|
|
65442
65442
|
"kind": "Project (non-normative maintainer notes)",
|
|
65443
65443
|
"summary": "cotal setup (implementations/cli/src/commands/setup.ts) saves configuration by default: it checks prerequisites, sets up the chosen application and runtimes, and seeds persona files.",
|
|
65444
|
-
"body": "# Setup internals (maintainer notes)\n\n> **Project** (non-normative maintainer notes) \xB7 **For:** maintainers changing how setup works\n>\n> How `cotal setup` works, and the cross-repo couplings it depends on. If you change one of\n> the things in the **Invariants** table, update the listed siblings in the same change, or\n> setup silently breaks for npx users.\n\n## The flow\n\n`cotal setup`\n([`implementations/cli/src/commands/setup.ts`](../implementations/cli/src/commands/setup.ts))\nsaves configuration by default: it checks prerequisites, sets up the chosen application and\nruntimes, and seeds persona files. Start is a separate terminal choice or `--start`; a script must\nalso pass `--detach`. The dashboard remains a separately configured `web` service preset. Every file it writes is announced (`\u2192 wrote \u2026` via `provenance.wrote`)\non stderr, or on stdout with the error when a stderr write fails. Each announcement is one line: a\ncontrol character or Unicode line separator in a path, such as a newline in `HOME`, is printed as\na `\\uXXXX` escape.\nEvery run takes the same inputs: destination, application, runtimes, project and space. With a\nterminal, `askInputs` asks for each one no flag gave and shows the answers before anything is\nwritten; Change asks again with the answers kept and Cancel saves nothing. Without one, or with\n`--yes`, `--no-input` or `--json`, `scriptedInputs` takes them from the flags and refuses a missing\none. `--destination cloud` is refused as in development before anything else. `runtimeChoices`\nlists `pty`, the official runtimes with the host tools in `OFFICIAL_RUNTIME_TOOLS`, and each\ninstalled third-party runtime. A script's runtimes are the `--runtime` names, which `runtimeFlags`\nchecks before any write: an unknown name or an official one whose tool is not on PATH is refused.\n\n`validateSetupArguments` rejects conflicting startup flags and non-detached scripted startup\nbefore seeding. `planSetup` checks preparation before it writes: the space name, the application against the setup\nconnector surface, and the space at its destination. A local space goes through `checkLocalSpace`\nin [`space-registration.ts`](../implementations/cli/src/commands/space-registration.ts), the\nchecks `space create` makes, which with setup's `again` reports a space already prepared or started\nat that root instead of refusing it; a local space also needs a `nats-server` to locate. A\nself-hosted space goes through `checkSelfHostedSpace`: a registration whose root is the project, or\na connection file `addFromBundle` will register.\n\n`prepare` then writes, in order: the prepared space or the registration, the `default` persona in\n`<project>/.cotal/agents` (plus the demo personas with `--demo`), the space's `harness`\npreference and your own when you have none, the application's `connector` and `mcpServers`\nactions, every present connector's `skills` action, `~/.agents/skills` when it is not current, the\nchosen runtimes, and the onboarded stamp. `setUpRuntimes` adds a chosen official runtime that is\nnot installed through `addExtension` at the CLI's version, the spec `cotal update` replays, then asks every chosen provider's\n`available()`; a missing tool, a failed install or a provider that does not answer makes the result\n`partial`. It never removes a runtime. Each write is\nrecorded in the result's `written` list, so a rerun that changes nothing is a `no-op`. The default\npersona comes from `seedDefaultPersona` in\n[`lib/default-persona.ts`](../implementations/cli/src/lib/default-persona.ts), which\n`space create` and a local `up` also call. An\napplication whose `requires` is not on PATH runs none of its actions, and the result is `partial`\nwith the rest saved. The rich presentation narrates actions through `runSteps`; `--json` and\n`--plain` run them quietly and keep stdout for the result. The old `--auth` / `--open` flags are\ngone: mode is `cotal up [--open]`'s concern.\n\n**`--skills`** writes skills only: it asks installed connectors with a declared skills setup\nhook to reconcile their own harness, then reconciles `~/.agents/skills`. The base CLI passes only the\nvendor-neutral skills directory, version, and state directory; connector packages own native assets\nand commands. It does not seed personas, install the mesh\nconnector, offer a global install, or write the onboarded stamp. Combined with another setup\ninput, `--full` or `--demo` it is refused.\n\nThe seeded `default` persona has an empty active `subscribe` set and wildcard\n`allowSubscribe`/`allowPublish` ACLs. A fresh agent receives no channel traffic until it joins a\nchannel, but can join, create, read, and post to channels on demand. The guided demo personas keep\ntheir existing `welcome` read and post scope. Repeat setup replaces the prior default template only\nwhen its bytes still match the shipped legacy body with `allowPublish: []`; any user edit makes the\nfile ineligible and leaves it byte-identical.\n\nSteps run in-process via `runSteps`\n([`lib/steps.ts`](../implementations/cli/src/lib/steps.ts)). A step can be `optional` (asked\nY/n), carry a `confirm` consent prompt, or be `live` (it draws its own pane via\n[`lib/live-window.ts`](../implementations/cli/src/lib/live-window.ts)). On failure, an\ninteractive run offers a debug handoff for each connector whose setup provider declares an `assist`\nand whose executables are on PATH\n([`lib/assist.ts`](../implementations/cli/src/lib/assist.ts)). The provider owns the harness\nbinary and its flags; the CLI only builds the prompt. When no connector can host one, the menu says\nso in one line. A provider may also declare `status`, which returns read-only rows about what it\ninstalled. `cotal status` prints them, and the CLI passes only its own version and\nthe `cotal setup --skills` remedy. The extensions manifest caches each connector's setup ref, so\nstatus imports only connectors that declare a provider. The seed reconcile refreshes a seeded entry\nwhose cache predates that ref.\n\nThe **application question** offers the **setup connector surface**\n(`setupConnectorSurface`): every connector name the live registry or the installed extension\nmanifest advertises, materialized through the same loader the rest of the CLI uses. No connector\nname is written into `setup.ts`. `setupConnectorCandidates` turns that surface into choices and\nreads each hint off the connector's own declarations: `requires` names the executables a candidate\nstill needs on PATH, `setup` says whether it owns setup actions at all, and `pluginRoot` says\nwhether those actions install plugin assets. The chosen candidate runs its connector-owned\n`connector` action as a narrated step built by `actionStep`, which takes the action with the input its\ntype declares, so the compiler checks each pairing. The provider side is checked too. Each action's\n`run`, `assist.run` and `status` are function-typed properties, which TypeScript checks strictly, so a\nprovider whose callback narrows the input the CLI hands it does not compile. A candidate\nthat declares no provider is simply marked ready (OpenCode auto-wires at spawn, injecting its\nplugin via `buildLaunch` and never writing the user's config). The chosen candidate's `mcpServers` action runs next\nthe same way: the Claude provider reads the user-scope servers from Claude Code's config and\nrecords them through the `seed` input the CLI hands it. That is workspace's `seedConnectorServers`,\nwhich writes the operator-level cotal config under a lock and only when it declares no list for that\nconnector, so two setups run at once record one list. The `skills` action runs for every\npresent connector that declares one, selected or not, because Cotal's authored skills are\nindependent of mesh membership. An upgrade also supplies the current authored skills to each\nrefresh action, which updates an existing skills installation without adding a removed one.\nWith `--demo`, two experts (david, the engineer; sven, the guide) plus the operator's own driving\nsession (`me`) are written too, and `me` is the persona\n`cotal spawn me` drives.\n\n**`--yes`** asks nothing even on a TTY, so every input must be a flag. Optional and `confirm`\nsteps run, the global install takes its default, and a failed action makes the result `partial`.\nIt still launches nothing; the control plane comes up with `cotal up --detach`. The scripted form\nis the agent/CI contract; keep it working.\n\n## Invariants\n\n| Thing | Must stay in sync across | Why |\n|---|---|---|\n| Marketplace name **`cotal-mesh`** | `MARKETPLACE` in the claude connector's [`src/setup.ts`](../extensions/connector-claude-code/src/setup.ts) (materialized `marketplace.json`), repo [`.claude-plugin/marketplace.json`](../.claude-plugin/marketplace.json) | Setup installs and verifies the plugin as `cotal@cotal-mesh`. The wake channel does not use this name: `CHANNEL_REF` in [`extension.ts`](../extensions/connector-claude-code/src/extension.ts) is `server:cotal` |\n| Plugin assets | the claude connector's own [`src/setup.ts`](../extensions/connector-claude-code/src/setup.ts) copy list `COTAL_ASSETS` (`dist/mcp.cjs`, `dist/hook.cjs`, `dist/monitor.cjs`, `.claude-plugin/plugin.json`, `.mcp.json`, `hooks/hooks.json`, `hooks/native-wake.js`, `monitors/monitors.json`, plus the `dist/cli.json` that setup writes beside them), its `package.json` `files` field, and the release tree's list in [`scripts/materialize-claude-plugin.mjs`](../scripts/materialize-claude-plugin.mjs) | The connector materializes its own plugin; missing or renamed assets break the install, and the base CLI has no copy of the list. The release script refuses a plugin whose `.mcp.json` or hooks reference a file it did not copy |\n| `cotal` plugin copy | `materializeCotal` and `refresh` in the claude connector's `src/setup.ts`; `pluginVersion` stamps the release, `+`, then a digest of every file setup copies, `cli.json` included | A repeat `cotal setup` and `cotal update` replace the materialized payload and update existing registrations with Claude's own plugin command. Refresh preserves disabled settings and verifies the recorded version and resolved files. Status checks the same payload rather than trusting the marketplace alone. Running sessions need a plugin reload or restart |\n| Claude Desktop entry | `desktopConfigPath` and the `cotal` server name in the claude connector's `src/desktop-config.ts`, and `desktopEntry` in its `src/setup.ts` | Desktop reads its config only when it starts. The entry runs the materialized `dist/mcp.cjs` with `--desktop`, so the materialize path and the entry must agree |\n| `Connector.pluginRoot` | [`packages/core/src/connector.ts`](../packages/core/src/connector.ts) (contract) plus set in the claude connector's `extension.ts` | A connector's declaration that it ships installable plugin assets; the picker phrases its hint from it |\n| `BUNDLED_PKG_PREFIX` | [`lib/nats-bin.ts`](../implementations/cli/src/lib/nats-bin.ts) \u2194 the `@eplightning/nats-server-*` `optionalDependencies` in [`implementations/cli/package.json`](../implementations/cli/package.json) | The bundled NATS binary is resolved by `${prefix}-${platform}-${arch}`. (Future: swap the prefix to our own `@cotal-ai/nats-server-*`.) |\n| Onboard marker plus `ONBOARD_VERSION` | `~/.cotal/onboarded.json` in [`lib/onboard.ts`](../implementations/cli/src/lib/onboard.ts); version const in `setup.ts` | Setup shows the splash until it is written |\n| Demo-agent format | `DEMO_AGENTS` in `setup.ts` matches the frontmatter shape read by [`packages/core/src/agent-file.ts`](../packages/core/src/agent-file.ts) (same as `examples/01-lateral-coordination/agents/`) | `cotal spawn <name>` loads these |\n| Managed personas | each `DEMO_AGENTS` body carries a `# managed by cotal-setup` frontmatter marker; `writeDemoAgent` refreshes the file when the body changes, backing a marker-less (user-edited) file up to `<name>.md.bak` first | Edit `DEMO_AGENTS` plus re-run setup to update david/sven/me; delete the marker line to take ownership |\n| `DEFAULT_SERVER` | [`packages/core/src/endpoint.ts`](../packages/core/src/endpoint.ts) | The address `cotal up` starts |\n\n## Background processes (`cotal up`)\n\n`cotal up` owns native local stack startup. Opt-in setup startup uses that same entry through\n`__setup-start`, with the prepared root, space and current CLI version checked before effects.\nThe child passes that root to native `up --root`; it does not rely on an ambient workspace-root\nenvironment variable or an ancestor walk. It runs without prompts, and its output is not evidence. A nonzero or unconfirmed\nreturn retains uncertainty; timeout or interruption sends no unverified process signal. A successful\nreturn supplies only command completion and the root's recorded manager selector. Setup then calls\n`launchManagedPersona`, whose original native join receipt is the agent-readiness fact. The shared\nobserver lets setup retain one finite JSON receipt rather than embedding a second spawn envelope.\nSelf-hosted setup starts no local replacement stack. An explicit `--on` is validated\nbefore preparation, reaches the selected manager's credential issuance and native launch,\nand is refused for local startup. Preparation and startup failures remain separate.\nThis composition does not complete the new whole-stack controller or privileged schema writer. The control plane comes up in cutover order:\nold-manager preflight \u2192 **delivery daemon** (auth mode only) \u2192 **manager**, via\n`ensureControlPlane`\n([`lib/delivery-proc.ts`](../implementations/cli/src/lib/delivery-proc.ts)). The preflight reads\nthe manager record once and judges that one process: it stops a live manager whose\n`manager.<key>.delivery-aware` marker does not hold its pid, and leaves one whose marker does or\nthat exited while the preflight looked. The stop signals only that pid. A manager that replaced the\nrecord in the meantime is left running, and the delivery daemon's own check judges it. The detached\nprocesses, all stopped by `cotal down`:\n\nWith no explicit `--server`, `cotal up` auto-selects a free local port when the default broker\naddress is already held by another root or an unrecorded broker; an explicit `--server` remains\nfail-loud on collision.\n\n- **Mesh:** `startMeshDetached`\n ([`commands/up.ts`](../implementations/cli/src/commands/up.ts)) boots the background\n nats-server for `up --detach` and `up -f`, and writes `.cotal/nats.pid` and `.cotal/nats.log`.\n Foreground `up` runs the broker as its own child. Both modes write `.cotal/nats.pid` as soon as\n the broker spawns, so `cotal down` can stop it when `up` dies before the broker is ready. When\n that write fails, `up` stops the broker, leaves whatever is at `.cotal/nats.pid` in place and\n exits with the write error. Both then run one spawn-to-ready sequence: readiness, the broker\n version read and the server name check of a resumed or restored listener. Then they run one\n listener-ready sequence: space setup, user-auth service, mesh record, transport policy, control\n plane. When the listener never answers, its broker is below the version floor, or the space setup\n of a fresh boot fails, `up` stops the listener before it exits. It sends SIGKILL when SIGTERM\n does not stop it and removes `.cotal/nats.pid` only once the listener has exited.\n- **Delivery daemon:** `startDeliveryDetached` / `ensureDelivery`\n ([`lib/delivery-proc.ts`](../implementations/cli/src/lib/delivery-proc.ts)) re-execs `cotal\n deliver` detached with a pre-minted scoped `delivery.creds` (auth mode only, the durable\n backstop; open mode has none). Writes `.cotal/delivery.<key>.pid` and `.cotal/delivery.<key>.log`,\n where `<key>` is the space key ([Config](config.md#project-files)), so a root can serve a space per\n daemon.\n- **Manager:** `startManagerDetached` / `ensureManager`\n ([`lib/manager-proc.ts`](../implementations/cli/src/lib/manager-proc.ts)) re-execs `cotal\n supervise` detached (pty runtime); it answers the control plane\n (`cotal_spawn` / `cotal_despawn` / `cotal_persona`). Writes `.cotal/manager.<key>.log`;\n `managerUp(space)` checks that space's pid record. The **manager itself**\n writes `.cotal/manager.<key>.pid`, so a supervisor started by a container entrypoint, by cron, or\n by hand is recorded the same way a detached `cotal up` is. Readers verify the recorded pid is alive and is a\n supervisor before trusting it ([Config](config.md#project-files)).\n\nThe **web dashboard** is *not* part of `cotal up`. It ships inside `cotal-ai` as the `@cotal-ai/web`\nextension and is seeded automatically by the boot reconcile, the same durable, version-locked path as\nthe built-in connectors (`SEEDED_EXTENSIONS`), so it always matches the CLI version and needs no\nseparate install. Configure its service definition and explicit protected credential before\n`cotal service start web`. The native child owns its PID claim and private full-launch bootstrap\nrecord. The preset is explicitly read-only and retains the registered TLS requirement. Its\ninitialization phase does not prove a new authenticated health response; user-auth caller handoff\nand OS installation remain unsupported. See [dashboard setup](watch-a-mesh.md#web-dashboard).\n\nAll recorded local processes self-register `local-process` descriptors. Bare `cotal down` resolves\nthe full set and stops it in dependency order. No command selects one descriptor: a named\n`cotal down <name>` stops a saved service definition. Installed extensions cache their contributed\nregistry keys, so the\nbase CLI does not hardcode optional package pidfiles.\n\nEach recorded pidfile also carries a sibling `<pidfile>.identity` pin (pid plus the process's\nstart, where the OS reports one). The pin proves that the live process is still the recorded one.\nA reused pid and a torn or unreadable pin are refused and preserved. On Windows, manager and\ndelivery stops use a private process-bound local capability to reach their cleanup handlers.\nA daemon without that capability must be stopped through its original supervisor before relaunch.\nComponents using signals warn on a pre-pin record; the next launch writes a pin.\nA record clears only once death is confirmed. Local cooperative control files live under\n`.cotal/local-stop/`, with generation-specific names and private file permissions. Injected hosted\ndelivery services do not create these files.\n\nAll re-execs resolve this CLI via `selfArgv()` / `selfCotal()`\n([`lib/self-exec.ts`](../implementations/cli/src/lib/self-exec.ts)) = `[node, ...loaderFlags,\nentry]` (tsx loader in dev, compiled JS in prod), so they never need `cotal` on PATH; the stack\ncomes up identically via `npx`, `npm i -g`, and a dev clone.\n\n`selfArgv()` throws unless `process.argv[1]` resolves, through any symlink, to the bin the\n`cotal-ai` package declares (`dist/cotal.js`) or to the `cotal.ts` beside that package's manifest\n(a checkout's `bin/cotal.ts`). Started from any other file, such as a smoke suite under tsx, a\nre-exec would run that file again with a subcommand it ignores, and a file that reaches a starter\non load would spawn its own successor (#1629). The auth, manager and delivery starters ask before\nthey touch a pidfile or a log, and `seedOne` asks before it writes its cursor, stages a payload or\nwrites its child marker, so a refusal on those paths leaves none of them behind.\n\nFor ergonomics only, an npx run with no global `cotal` offers to `npm i -g cotal-ai`\n(`offerGlobalInstall`, pinned to the running version): gated on `isNpx()` plus a PATH scan\n(`cotalOnPath()`, not an exec probe, since `cotal --version` is not a real command). The\ninteractive prompt defaults to yes, the non-interactive path (`--yes` or no TTY) takes the\ndefault, and a failed install is non-fatal (warn plus manual command). It is offered only in the\nrich presentation, so `--json` and `--plain` keep stdout for the result. The same `self-exec.ts`\nexposes `displayCmd()`, the prefix (`cotal` / `npx cotal-ai` / `pnpm cotal`) used in setup's next\ncommand so it matches how you ran it.\n\n## Built-in connectors are seeded extensions\n\nThe first-party connectors (`claude`, `opencode`, `codex`, `hermes`, `jcode`, `pi`) are **not** static-imported by\nthe binary. The composition root (`bin/cotal.ts`) registers no connector; they self-register only when\nimported, and they are imported only once installed. Before the first command of each boot that is\nnot a read, the CLI **seeds** them through the same `cotal ext add` path a third party uses, so they\nare ordinary extensions you can `cotal ext remove`. A command or subcommand that declares `reads`\ninstalls and refreshes nothing, and the CLI never imports a built-in whose installed version differs\nfrom the binary's: it names the package with `cotal ext seed` instead. Code lives in [`implementations/cli/src/seed/`](../implementations/cli/src/seed/);\nthe entry is `reconcileSeededConnectors()`, gated in `runCli` before the manifest overlay so\n`ext seed --repair` survives a corrupt manifest.\n\n**What ships where.** The connectors are `devDependencies` of `cotal-ai` (not runtime deps), and a\n`prepack` step ([`bin/scripts/copy-seeded-connectors.mjs`](../bin/scripts/copy-seeded-connectors.mjs))\n`npm pack`s each into `bin/seeded-connectors/<name>/` (honoring each connector's own `files`), added to\nthe package `files`. `SEEDED_EXTENSIONS` (`@cotal-ai/workspace`) is the shared list: the\nconnectors plus `web`. The prepack asserts that every bundled payload's `name` and `version` match\nthe umbrella (the\n`fixed` changeset group keeps them lockstep), so a version-skewed payload can never be published; `web`\nalso emits `dist/web/vendor/vendor-manifest.json` (name/version/license/sha512) as the auditable\ninventory of its vendored browser libs (marked/DOMPurify ship as opaque `dist` bytes, not runtime deps).\n`seed/paths.ts:shippedSourceDir` resolves the live `extensions/<pkg>` dir in a\nsource checkout and `<cotal-ai>/seeded-connectors/<name>` in a published install. The reconcile copies\nthat payload into the durable store `seed/store/<version>/<name>`. The version is validated as one safe\npath segment, and the destination is checked to stay inside the store before anything is written. `ext add --install-links` reifies\nthe `file:` dep from THAT stable path (a volatile source would fail to re-reify); `ext add` then\njunction-links each `@cotal-ai/*` peer to the binary's own copy. Before the first lazy import in each\nprocess, materialization rechecks those links by realpath and rebinds stale links under the extension\nlock. This lets the registry-facing imports of a global install, npx, and source worktrees share the\nmachine prefix while each process still gets its host's single `@cotal-ai/core` registry instance;\nlauncher artifacts are self-contained and do not resolve those mutable links later.\n\n**Reconcile policy** (generation = the `cotal-ai` version): a never-seeded built-in is seeded; a\nstill-installed one WE seeded (`source: \"seeded\"`) is refreshed only when the version bumps (semver\ncompare) or under `--force`; an operator-managed official entry (a manual `ext add` at a chosen\nversion, no seeded marker) is left untouched on upgrade; a deliberately-removed one stays removed. The\n`ever-seeded` **authority** (`seed/authority.json`, mirrored to a monotonic `.bak`) is the sole arbiter\nof removed-vs-never-seeded and is unioned with its backup on read, so a truncated authority never\nresurrects a removal. Before writing the generation stamp, setup verifies that every\n(re)installed extension is recorded in the manifest, present on disk with a resolvable entry file,\nand at the generation version. A version-skewed payload fails loud (`ext seed --repair`) rather than being stamped as current. A cotal\n**older** than the store's stamped generation refuses before writing anything, rather than stamping the\nstore back down to its own version while refreshing nothing: run the newer cotal, or `ext seed --force`\nto rebuild the store for the version you are running. `--reset` is not that recovery: it discards the\never-seeded authority and resurrects deliberately-removed connectors. The refusal names a concrete cotal executable\nonly after a bounded `--version` probe proves that executable is at least the store generation;\notherwise it retains the generic instruction. A generation advance records the exact\nrealpath-resolved CLI entry and an ISO timestamp in `seed/stamp.json`, then announces the migration\nafter that stamp commits. An older CLI includes those fields in its refusal when present; legacy\ngeneration-only stamps stay valid and retain the shorter refusal. A CLI whose package root is the\nrepo `bin/` (a source checkout, including a suite child of `bin/cotal.ts`) refuses that write, stamp,\nand generation GC rather than migrating the operator-global store. The refusal names\n`COTAL_SKIP_CONNECTOR_SEED=1` as the way to run other commands from a checkout, since an isolated\n`$XDG_CONFIG_HOME` alone does not lift it; `COTAL_HOME` does not relocate this store. Isolated\nin-tree seed smokes set `COTAL_ALLOW_CHECKOUT_SEED=1` after pointing `$XDG_CONFIG_HOME` at a scratch\ndir. An unproven entry is refused the same way: a missing identity answer is not treated as a\nreleased install.\n\nBefore an upgraded generation commits, reconciliation also refreshes already installed harness\ncopies through each connector's setup provider. The provider runs even when its native executable\nis unavailable: an installed integration that cannot be updated must fail, not disappear from the\nupgrade checks. A refresh failure leaves the older stamp and recovery cursor intact. The reconcile\nlock stays held while the extension writer lock is released for provider loading and native refresh,\nthen reacquired. A manifest change during that interval refuses the generation commit. This keeps\nordinary first use after an npm upgrade on the same harness-refresh path as `cotal update`.\n\nConnector import and registration do not probe Claude. Setup discovers readiness through the\nprovider, including Desktop's bundled executable, rather than the managed launch requirements.\nClaude's version probes have a five-second kill timeout. An unavailable unrelated harness is\nskipped during setup. Refresh reads Cotal's native plugin records first: no installed integration\nmeans no native probe, while an installed integration with an unusable executable still refuses.\nSelected setup actions recheck readiness inside the recorded step. Losing it after discovery\nproduces a partial result; no skipped native action is reported as completed.\n\nClaude's generated MCP entry and classic hook commands use the recorded Node executable. Those\ngenerated bytes, the native module and the CLI record all contribute to the plugin version digest.\n\n**Crash safety.** One shared advisory lock ([`packages/workspace/src/advisory-lock.ts`](../packages/workspace/src/advisory-lock.ts):\natomic hard-link publish, PID + process-start liveness, bounded wait, dead-owner reclaim) guards the\nwhole reconcile and every `cotal ext` mutation; a live reconcile is waited on, not mistaken for a crash.\nA crash **cursor** is journaled before each connector mutation and cleared only at the final commit, so\na SIGKILL mid-run is detected on the next boot (fail loud \u2192 `ext seed --repair` re-installs the\ninterrupted connector before it clears the evidence). Seed children are authenticated (they carry the\nlive lock's nonce + parent PID, not a bare env flag) and record a liveness marker so a post-crash repair\nrefuses to race an orphaned installer. `ext seed --reset` quarantines corrupt manifest/authority state\naside and rebuilds. See [cli.md `ext`](cli.md#ext) for the operator-facing flags.\n"
|
|
65444
|
+
"body": "# Setup internals (maintainer notes)\n\n> **Project** (non-normative maintainer notes) \xB7 **For:** maintainers changing how setup works\n>\n> How `cotal setup` works, and the cross-repo couplings it depends on. If you change one of\n> the things in the **Invariants** table, update the listed siblings in the same change, or\n> setup silently breaks for npx users.\n\n## The flow\n\n`cotal setup`\n([`implementations/cli/src/commands/setup.ts`](../implementations/cli/src/commands/setup.ts))\nsaves configuration by default: it checks prerequisites, sets up the chosen application and\nruntimes, and seeds persona files. Start is a separate terminal choice or `--start`; a script must\nalso pass `--detach`. The dashboard remains a separately configured `web` service preset. Every file it writes is announced (`\u2192 wrote \u2026` via `provenance.wrote`)\non stderr, or on stdout with the error when a stderr write fails. Each announcement is one line: a\ncontrol character or Unicode line separator in a path, such as a newline in `HOME`, is printed as\na `\\uXXXX` escape.\nEvery run takes the same inputs: destination, application, runtimes, project and space. With a\nterminal, `askInputs` asks for each one no flag gave and shows the answers before anything is\nwritten; Change asks again with the answers kept and Cancel saves nothing. Without one, or with\n`--yes`, `--no-input` or `--json`, `scriptedInputs` takes them from the flags and refuses a missing\none. `--destination cloud` is refused as in development before anything else. `runtimeChoices`\nlists `pty`, the official runtimes with the host tools in `OFFICIAL_RUNTIME_TOOLS`, and each\ninstalled third-party runtime. A script's runtimes are the `--runtime` names, which `runtimeFlags`\nchecks before any write: an unknown name or an official one whose tool is not on PATH is refused.\n\n`validateSetupArguments` rejects conflicting startup flags and non-detached scripted startup\nbefore seeding. `planSetup` checks preparation before it writes: the space name, the application against the setup\nconnector surface, and the space at its destination. A local space goes through `checkLocalSpace`\nin [`space-registration.ts`](../implementations/cli/src/commands/space-registration.ts), the\nchecks `space create` makes, which with setup's `again` reports a space already prepared or started\nat that root instead of refusing it; a local space also needs a `nats-server` to locate. A\nself-hosted space goes through `checkSelfHostedSpace`: a registration whose root is the project, or\na connection file `addFromBundle` will register.\n\n`prepare` then writes, in order: the prepared space or the registration, the `default` persona in\n`<project>/.cotal/agents` (plus the demo personas with `--demo`), the space's `harness`\npreference and your own when you have none, the application's `connector` and `mcpServers`\nactions, every present connector's `skills` action, `~/.agents/skills` when it is not current, the\nchosen runtimes, and the onboarded stamp. `setUpRuntimes` adds a chosen official runtime that is\nnot installed through `addExtension` at the CLI's version, the spec `cotal update` replays, then asks every chosen provider's\n`available()`; a missing tool, a failed install or a provider that does not answer makes the result\n`partial`. It never removes a runtime. Each write is\nrecorded in the result's `written` list, so a rerun that changes nothing is a `no-op`. The default\npersona comes from `seedDefaultPersona` in\n[`lib/default-persona.ts`](../implementations/cli/src/lib/default-persona.ts), which\n`space create` and a local `up` also call. An\napplication whose `requires` is not on PATH runs none of its actions, and the result is `partial`\nwith the rest saved. The rich presentation narrates actions through `runSteps`; `--json` and\n`--plain` run them quietly and keep stdout for the result. The old `--auth` / `--open` flags are\ngone: mode is `cotal up [--open]`'s concern.\n\n**`--skills`** writes skills only: it asks installed connectors with a declared skills setup\nhook to reconcile their own harness, then reconciles `~/.agents/skills`. The base CLI passes only the\nvendor-neutral skills directory, version, and state directory; connector packages own native assets\nand commands. It does not seed personas, install the mesh\nconnector, offer a global install, or write the onboarded stamp. Combined with another setup\ninput, `--full` or `--demo` it is refused.\n\nThe seeded `default` persona has an empty active `subscribe` set and wildcard\n`allowSubscribe`/`allowPublish` ACLs. A fresh agent receives no channel traffic until it joins a\nchannel, but can join, create, read, and post to channels on demand. The guided demo personas keep\ntheir existing `welcome` read and post scope. Repeat setup replaces the prior default template only\nwhen its bytes still match the shipped legacy body with `allowPublish: []`; any user edit makes the\nfile ineligible and leaves it byte-identical.\n\nSteps run in-process via `runSteps`\n([`lib/steps.ts`](../implementations/cli/src/lib/steps.ts)). A step can be `optional` (asked\nY/n), carry a `confirm` consent prompt, or be `live` (it draws its own pane via\n[`lib/live-window.ts`](../implementations/cli/src/lib/live-window.ts)). On failure, an\ninteractive run offers a debug handoff for each connector whose setup provider declares an `assist`\nand whose executables are on PATH\n([`lib/assist.ts`](../implementations/cli/src/lib/assist.ts)). The provider owns the harness\nbinary and its flags; the CLI only builds the prompt. When no connector can host one, the menu says\nso in one line. A provider may also declare `status`, which returns read-only rows about what it\ninstalled. `cotal status` prints them, and the CLI passes only its own version and\nthe `cotal setup --skills` remedy. The extensions manifest caches each connector's setup ref, so\nstatus imports only connectors that declare a provider. The seed reconcile refreshes a seeded entry\nwhose cache predates that ref.\n\nThe **application question** offers the **setup connector surface**\n(`setupConnectorSurface`): every connector name the live registry or the installed extension\nmanifest advertises, materialized through the same loader the rest of the CLI uses. No connector\nname is written into `setup.ts`. `setupConnectorCandidates` turns that surface into choices and\nreads each hint off the connector's own declarations: `requires` names the executables a candidate\nstill needs on PATH, `setup` says whether it owns setup actions at all, and `pluginRoot` says\nwhether those actions install plugin assets. The chosen candidate runs its connector-owned\n`connector` action as a narrated step built by `actionStep`, which takes the action with the input its\ntype declares, so the compiler checks each pairing. The provider side is checked too. Each action's\n`run`, `assist.run` and `status` are function-typed properties, which TypeScript checks strictly, so a\nprovider whose callback narrows the input the CLI hands it does not compile. A candidate\nthat declares no provider is simply marked ready (OpenCode auto-wires at spawn, injecting its\nplugin via `buildLaunch` and never writing the user's config). The chosen candidate's `mcpServers` action runs next\nthe same way: the Claude provider reads the user-scope servers from Claude Code's config and\nrecords them through the `seed` input the CLI hands it. That is workspace's `seedConnectorServers`,\nwhich writes the operator-level cotal config under a lock and only when it declares no list for that\nconnector, so two setups run at once record one list. The `skills` action runs for every\npresent connector that declares one, selected or not, because Cotal's authored skills are\nindependent of mesh membership. An upgrade also supplies the current authored skills to each\nrefresh action, which updates an existing skills installation without adding a removed one.\nWith `--demo`, two experts (david, the engineer; sven, the guide) plus the operator's own driving\nsession (`me`) are written too, and `me` is the persona\n`cotal spawn me` drives.\n\n**`--yes`** asks nothing even on a TTY, so every input must be a flag. Optional and `confirm`\nsteps run, the global install takes its default, and a failed action makes the result `partial`.\nIt still launches nothing; the control plane comes up with `cotal up --detach`. The scripted form\nis the agent/CI contract; keep it working.\n\n## Invariants\n\n| Thing | Must stay in sync across | Why |\n|---|---|---|\n| Marketplace name **`cotal-mesh`** | `MARKETPLACE` in the claude connector's [`src/setup.ts`](../extensions/connector-claude-code/src/setup.ts) (materialized `marketplace.json`), repo [`.claude-plugin/marketplace.json`](../.claude-plugin/marketplace.json) | Setup installs and verifies the plugin as `cotal@cotal-mesh`. The wake channel does not use this name: `CHANNEL_REF` in [`extension.ts`](../extensions/connector-claude-code/src/extension.ts) is `server:cotal` |\n| Plugin assets | the claude connector's own [`src/setup.ts`](../extensions/connector-claude-code/src/setup.ts) copy list `COTAL_ASSETS` (`dist/mcp.cjs`, `dist/hook.cjs`, `dist/monitor.cjs`, `.claude-plugin/plugin.json`, `.mcp.json`, `hooks/hooks.json`, `hooks/native-wake.js`, `monitors/monitors.json`, plus the `dist/cli.json` that setup writes beside them), its `package.json` `files` field, and the release tree's list in [`scripts/materialize-claude-plugin.mjs`](../scripts/materialize-claude-plugin.mjs) | The connector materializes its own plugin; missing or renamed assets break the install, and the base CLI has no copy of the list. The release script refuses a plugin whose `.mcp.json` or hooks reference a file it did not copy |\n| `cotal` plugin copy | `materializeCotal` and `refresh` in the claude connector's `src/setup.ts`; `pluginVersion` stamps the release, `+`, then a digest of every file setup copies, `cli.json` included | A repeat `cotal setup` and `cotal update` replace the materialized payload and update existing registrations with Claude's own plugin command. Refresh preserves disabled settings and verifies the recorded version and resolved files. Status checks the same payload rather than trusting the marketplace alone. Running sessions need a plugin reload or restart |\n| Claude Desktop entry | `desktopConfigPath` and the `cotal` server name in the claude connector's `src/desktop-config.ts`, and `desktopEntry` in its `src/setup.ts` | Desktop reads its config only when it starts. The entry runs the materialized `dist/mcp.cjs` with `--desktop`, so the materialize path and the entry must agree |\n| `Connector.pluginRoot` | [`packages/core/src/connector.ts`](../packages/core/src/connector.ts) (contract) plus set in the claude connector's `extension.ts` | A connector's declaration that it ships installable plugin assets; the picker phrases its hint from it |\n| `BUNDLED_PKG_PREFIX` | [`lib/nats-bin.ts`](../implementations/cli/src/lib/nats-bin.ts) \u2194 the `@eplightning/nats-server-*` `optionalDependencies` in [`implementations/cli/package.json`](../implementations/cli/package.json) | The bundled NATS binary is resolved by `${prefix}-${platform}-${arch}`. (Future: swap the prefix to our own `@cotal-ai/nats-server-*`.) |\n| Onboard marker plus `ONBOARD_VERSION` | `~/.cotal/onboarded.json` in [`lib/onboard.ts`](../implementations/cli/src/lib/onboard.ts); version const in `setup.ts` | Setup shows the splash until it is written |\n| Demo-agent format | `DEMO_AGENTS` in `setup.ts` matches the frontmatter shape read by [`packages/core/src/agent-file.ts`](../packages/core/src/agent-file.ts) (same as `examples/01-lateral-coordination/agents/`) | `cotal spawn <name>` loads these |\n| Managed personas | each `DEMO_AGENTS` body carries a `# managed by cotal-setup` frontmatter marker; `writeDemoAgent` refreshes the file when the body changes, backing a marker-less (user-edited) file up to `<name>.md.bak` first | Edit `DEMO_AGENTS` plus re-run setup to update david/sven/me; delete the marker line to take ownership |\n| `DEFAULT_SERVER` | [`packages/core/src/endpoint.ts`](../packages/core/src/endpoint.ts) | The address `cotal up` starts |\n\n## Background processes (`cotal up`)\n\n`cotal up` owns native local stack startup. Opt-in setup startup uses that same entry through\n`__setup-start`, with the prepared root, space and current CLI version checked before effects.\nThe child passes that root to native `up --root`; it does not rely on an ambient workspace-root\nenvironment variable or an ancestor walk. It runs without prompts, and its output is not evidence. A nonzero or unconfirmed\nreturn retains uncertainty; timeout or interruption sends no unverified process signal. A successful\nreturn supplies only command completion and the root's recorded manager selector. Setup then calls\n`launchManagedPersona`, whose original native join receipt is the agent-readiness fact. The shared\nobserver lets setup retain one finite JSON receipt rather than embedding a second spawn envelope.\nSelf-hosted setup starts no local replacement stack. An explicit `--on` is validated\nbefore preparation, reaches the selected manager's credential issuance and native launch,\nand is refused for local startup. Preparation and startup failures remain separate.\nThis composition does not complete the new whole-stack controller or privileged schema writer. The control plane comes up in cutover order:\nold-manager preflight \u2192 **delivery daemon** (auth mode only) \u2192 **manager**, via\n`ensureControlPlane`\n([`lib/delivery-proc.ts`](../implementations/cli/src/lib/delivery-proc.ts)). The preflight reads\nthe manager record once and judges that one process: it stops a live manager whose\n`manager.<key>.delivery-aware` marker does not hold its pid, and leaves one whose marker does or\nthat exited while the preflight looked. The stop signals only that pid. A manager that replaced the\nrecord in the meantime is left running, and the delivery daemon's own check judges it. The detached\nprocesses, all stopped by `cotal down`:\n\nWith no explicit `--server`, `cotal up` auto-selects a free local port when the default broker\naddress is already held by another root or an unrecorded broker; an explicit `--server` remains\nfail-loud on collision.\n\n- **Mesh:** `startMeshDetached`\n ([`commands/up.ts`](../implementations/cli/src/commands/up.ts)) boots the background\n nats-server for `up --detach` and `up -f`, and writes `.cotal/nats.pid` and `.cotal/nats.log`.\n Foreground `up` runs the broker as its own child. Both modes write `.cotal/nats.pid` as soon as\n the broker spawns, so `cotal down` can stop it when `up` dies before the broker is ready. When\n that write fails, `up` stops the broker, leaves whatever is at `.cotal/nats.pid` in place and\n exits with the write error. Both then run one spawn-to-ready sequence: readiness, the broker\n version read and the server name check of a resumed or restored listener. Then they run one\n listener-ready sequence: space setup, user-auth service, mesh record, transport policy, control\n plane. When the listener never answers, its broker is below the version floor, or the space setup\n of a fresh boot fails, `up` stops the listener before it exits. It sends SIGKILL when SIGTERM\n does not stop it and removes `.cotal/nats.pid` only once the listener has exited.\n- **Delivery daemon:** `startDeliveryDetached` / `ensureDelivery`\n ([`lib/delivery-proc.ts`](../implementations/cli/src/lib/delivery-proc.ts)) re-execs `cotal\n deliver` detached with a pre-minted scoped `delivery.creds` (auth mode only, the durable\n backstop; open mode has none). Writes `.cotal/delivery.<key>.pid` and `.cotal/delivery.<key>.log`,\n where `<key>` is the space key ([Config](config.md#project-files)), so a root can serve a space per\n daemon.\n- **Manager:** `startManagerDetached` / `ensureManager`\n ([`lib/manager-proc.ts`](../implementations/cli/src/lib/manager-proc.ts)) re-execs `cotal\n supervise` detached (pty runtime); it answers the control plane\n (`cotal_spawn` / `cotal_despawn` / `cotal_persona`). Writes `.cotal/manager.<key>.log`;\n `managerUp(space)` checks that space's pid record. The **manager itself**\n writes `.cotal/manager.<key>.pid`, so a supervisor started by a container entrypoint, by cron, or\n by hand is recorded the same way a detached `cotal up` is. Readers verify the recorded pid is alive and is a\n supervisor before trusting it ([Config](config.md#project-files)).\n\nThe **web dashboard** is *not* part of `cotal up`. It ships inside `cotal-ai` as the `@cotal-ai/web`\nextension and is seeded automatically by the boot reconcile, the same durable, version-locked path as\nthe built-in connectors (`SEEDED_EXTENSIONS`), so it always matches the CLI version and needs no\nseparate install. Configure its service definition and explicit protected credential before\n`cotal service start web`. The native child owns its PID claim and private full-launch bootstrap\nrecord. The preset is explicitly read-only and retains the registered TLS requirement. Its\ninitialization phase does not prove a new authenticated health response; user-auth caller handoff\nand OS installation remain unsupported. See [dashboard setup](watch-a-mesh.md#web-dashboard).\n\nAll recorded local processes self-register `local-process` descriptors. Bare `cotal down` resolves\nthe full set and stops it in dependency order. No command selects one descriptor: a named\n`cotal down <name>` stops a saved service definition. Installed extensions cache their contributed\nregistry keys, so the\nbase CLI does not hardcode optional package pidfiles.\n\nEach recorded pidfile also carries a sibling `<pidfile>.identity` pin (pid plus the process's\nstart, where the OS reports one). The pin proves that the live process is still the recorded one.\nA reused pid and a torn or unreadable pin are refused and preserved. On Windows, manager and\ndelivery stops use a private process-bound local capability to reach their cleanup handlers.\nA daemon without that capability must be stopped through its original supervisor before relaunch.\nComponents using signals warn on a pre-pin record; the next launch writes a pin.\nA record clears only once death is confirmed. Local cooperative control files live under\n`.cotal/local-stop/`, with generation-specific names and private file permissions. Injected hosted\ndelivery services do not create these files.\n\nAll re-execs resolve this CLI via `selfArgv()` / `selfCotal()`\n([`lib/self-exec.ts`](../implementations/cli/src/lib/self-exec.ts)) = `[node, ...loaderFlags,\nentry]` (tsx loader in dev, compiled JS in prod), so they never need `cotal` on PATH; the stack\ncomes up identically via `npx`, `npm i -g`, and a dev clone.\n\n`selfArgv()` throws unless `process.argv[1]` resolves, through any symlink, to the bin the\n`cotal-ai` package declares (`dist/cotal.js`) or to the `cotal.ts` beside that package's manifest\n(a checkout's `bin/cotal.ts`). Started from any other file, such as a smoke suite under tsx, a\nre-exec would run that file again with a subcommand it ignores, and a file that reaches a starter\non load would spawn its own successor (#1629). The auth, manager and delivery starters ask before\nthey touch a pidfile or a log, and `seedOne` asks before it writes its cursor, stages a payload or\nwrites its child marker, so a refusal on those paths leaves none of them behind.\n\nFor ergonomics only, an npx run with no global `cotal` offers to `npm i -g cotal-ai`\n(`offerGlobalInstall`, pinned to the running version): gated on `isNpx()` plus a PATH scan\n(`cotalOnPath()`). `displayCmd()` runs the same scan for every user-facing hint, so the scan only\nresolves files. The `cotal --version` exec probe stays in `verifiedCotalExecutables()`, which the\nrefusal of an older cotal (below) uses to name a newer executable. The interactive prompt defaults\nto yes, the non-interactive path (`--yes` or no TTY) takes the default, and a failed install is\nnon-fatal (warn plus manual command). It is offered only in the rich presentation, so `--json` and\n`--plain` keep stdout for the result. The same `self-exec.ts`\nexposes `displayCmd()`, the prefix (`cotal` / `npx cotal-ai` / `pnpm cotal`) used in setup's next\ncommand so it matches how you ran it.\n\n## Built-in connectors are seeded extensions\n\nThe first-party connectors (`claude`, `opencode`, `codex`, `hermes`, `jcode`, `pi`) are **not** static-imported by\nthe binary. The composition root (`bin/cotal.ts`) registers no connector; they self-register only when\nimported, and they are imported only once installed. Before the first command of each boot that is\nnot a read, the CLI **seeds** them through the same `cotal ext add` path a third party uses, so they\nare ordinary extensions you can `cotal ext remove`. A command or subcommand that declares `reads`\ninstalls and refreshes nothing, and the CLI never imports a built-in whose installed version differs\nfrom the binary's: it names the package with `cotal ext seed` instead. Code lives in [`implementations/cli/src/seed/`](../implementations/cli/src/seed/);\nthe entry is `reconcileSeededConnectors()`, gated in `runCli` before the manifest overlay so\n`ext seed --repair` survives a corrupt manifest.\n\n**What ships where.** The connectors are `devDependencies` of `cotal-ai` (not runtime deps), and a\n`prepack` step ([`bin/scripts/copy-seeded-connectors.mjs`](../bin/scripts/copy-seeded-connectors.mjs))\n`npm pack`s each into `bin/seeded-connectors/<name>/` (honoring each connector's own `files`), added to\nthe package `files`. `SEEDED_EXTENSIONS` (`@cotal-ai/workspace`) is the shared list: the\nconnectors plus `web`. The prepack asserts that every bundled payload's `name` and `version` match\nthe umbrella (the\n`fixed` changeset group keeps them lockstep), so a version-skewed payload can never be published; `web`\nalso emits `dist/web/vendor/vendor-manifest.json` (name/version/license/sha512) as the auditable\ninventory of its vendored browser libs (marked/DOMPurify ship as opaque `dist` bytes, not runtime deps).\n`seed/paths.ts:shippedSourceDir` resolves the live `extensions/<pkg>` dir in a\nsource checkout and `<cotal-ai>/seeded-connectors/<name>` in a published install. The reconcile copies\nthat payload into the durable store `seed/store/<version>/<name>`. The version is validated as one safe\npath segment, and the destination is checked to stay inside the store before anything is written. `ext add --install-links` reifies\nthe `file:` dep from THAT stable path (a volatile source would fail to re-reify); `ext add` then\njunction-links each `@cotal-ai/*` peer to the binary's own copy. Before the first lazy import in each\nprocess, materialization rechecks those links by realpath and rebinds stale links under the extension\nlock. This lets the registry-facing imports of a global install, npx, and source worktrees share the\nmachine prefix while each process still gets its host's single `@cotal-ai/core` registry instance;\nlauncher artifacts are self-contained and do not resolve those mutable links later.\n\n**Reconcile policy** (generation = the `cotal-ai` version): a never-seeded built-in is seeded; a\nstill-installed one WE seeded (`source: \"seeded\"`) is refreshed only when the version bumps (semver\ncompare) or under `--force`; an operator-managed official entry (a manual `ext add` at a chosen\nversion, no seeded marker) is left untouched on upgrade; a deliberately-removed one stays removed. The\n`ever-seeded` **authority** (`seed/authority.json`, mirrored to a monotonic `.bak`) is the sole arbiter\nof removed-vs-never-seeded and is unioned with its backup on read, so a truncated authority never\nresurrects a removal. Before writing the generation stamp, setup verifies that every\n(re)installed extension is recorded in the manifest, present on disk with a resolvable entry file,\nand at the generation version. A version-skewed payload fails loud (`ext seed --repair`) rather than being stamped as current. A cotal\n**older** than the store's stamped generation refuses before writing anything, rather than stamping the\nstore back down to its own version while refreshing nothing: run the newer cotal, or `ext seed --force`\nto rebuild the store for the version you are running. `--reset` is not that recovery: it discards the\never-seeded authority and resurrects deliberately-removed connectors. The refusal names a concrete cotal executable\nonly after a bounded `--version` probe proves that executable is at least the store generation;\notherwise it retains the generic instruction. A generation advance records the exact\nrealpath-resolved CLI entry and an ISO timestamp in `seed/stamp.json`, then announces the migration\nafter that stamp commits. An older CLI includes those fields in its refusal when present; legacy\ngeneration-only stamps stay valid and retain the shorter refusal. A CLI whose package root is the\nrepo `bin/` (a source checkout, including a suite child of `bin/cotal.ts`) refuses that write, stamp,\nand generation GC rather than migrating the operator-global store. The refusal names\n`COTAL_SKIP_CONNECTOR_SEED=1` as the way to run other commands from a checkout, since an isolated\n`$XDG_CONFIG_HOME` alone does not lift it; `COTAL_HOME` does not relocate this store. Isolated\nin-tree seed smokes set `COTAL_ALLOW_CHECKOUT_SEED=1` after pointing `$XDG_CONFIG_HOME` at a scratch\ndir. An unproven entry is refused the same way: a missing identity answer is not treated as a\nreleased install.\n\nBefore an upgraded generation commits, reconciliation also refreshes already installed harness\ncopies through each connector's setup provider. The provider runs even when its native executable\nis unavailable: an installed integration that cannot be updated must fail, not disappear from the\nupgrade checks. A refresh failure leaves the older stamp and recovery cursor intact. The reconcile\nlock stays held while the extension writer lock is released for provider loading and native refresh,\nthen reacquired. A manifest change during that interval refuses the generation commit. This keeps\nordinary first use after an npm upgrade on the same harness-refresh path as `cotal update`.\n\nConnector import and registration do not probe Claude. Setup discovers readiness through the\nprovider, including Desktop's bundled executable, rather than the managed launch requirements.\nClaude's version probes have a five-second kill timeout. An unavailable unrelated harness is\nskipped during setup. Refresh reads Cotal's native plugin records first: no installed integration\nmeans no native probe, while an installed integration with an unusable executable still refuses.\nSelected setup actions recheck readiness inside the recorded step. Losing it after discovery\nproduces a partial result; no skipped native action is reported as completed.\n\nClaude's generated MCP entry and classic hook commands use the recorded Node executable. Those\ngenerated bytes, the native module and the CLI record all contribute to the plugin version digest.\n\n**Crash safety.** One shared advisory lock ([`packages/workspace/src/advisory-lock.ts`](../packages/workspace/src/advisory-lock.ts):\natomic hard-link publish, PID + process-start liveness, bounded wait, dead-owner reclaim) guards the\nwhole reconcile and every `cotal ext` mutation; a live reconcile is waited on, not mistaken for a crash.\nA crash **cursor** is journaled before each connector mutation and cleared only at the final commit, so\na SIGKILL mid-run is detected on the next boot (fail loud \u2192 `ext seed --repair` re-installs the\ninterrupted connector before it clears the evidence). Seed children are authenticated (they carry the\nlive lock's nonce + parent PID, not a bare env flag) and record a liveness marker so a post-crash repair\nrefuses to race an orphaned installer. `ext seed --reset` quarantines corrupt manifest/authority state\naside and rebuilds. See [cli.md `ext`](cli.md#ext) for the operator-facing flags.\n"
|
|
65445
65445
|
},
|
|
65446
65446
|
{
|
|
65447
65447
|
"slug": "spaces",
|
|
@@ -65476,7 +65476,7 @@ function loadDocsBundle() {
|
|
|
65476
65476
|
"title": "Upgrading a running deployment",
|
|
65477
65477
|
"kind": "Guide (informative)",
|
|
65478
65478
|
"summary": "Substrate stability tells you what the version numbers promise.",
|
|
65479
|
-
"body": "# Upgrading a running deployment\n\n> **Guide** (informative) \xB7 **For:** operators upgrading a mesh that already exists \xB7 **See also:** [Substrate stability](stability.md), [Run a mesh](run-a-mesh.md), [Identity and auth](identity-and-auth.md)\n\n[Substrate stability](stability.md) tells you what the version numbers promise. This page is the\nother half: what to actually do when the deployment already exists, has credentials in it, and\ncannot simply be recreated. Every release that breaks a running deployment gets a section here,\nnaming what migrates on its own, what does not, and the order to move the pieces in.\n\n## The pre-1.0 upgrade contract\n\nThe packages are pre-1.0, so a minor bump may break an API or an on-disk expectation. Four\ncommitments make that survivable for someone with a fleet:\n\n- **Pin an exact version.** `0.N.P`, never `^0.N.P`. A range can pull a breaking minor in during an\n unrelated reinstall.\n- **Every break that touches a running deployment gets a section on this page**, written in terms of\n what an operator does, not in terms of which module changed.\n- **Read the section before you start, not halfway through.** A section names the work up front\n precisely so the operation does not change shape once it is underway.\n- **A break that cannot be made automatic says so.** Where credentials or state must be recreated by\n hand, the section says which ones and when, rather than leaving you to discover it at the moment\n the first one stops working.\n- **A change to the shape of a credential, or to who may renew one, is breaking whatever the commit\n marker says.** This rule is stated because the marker is a judgement made while writing the code\n and the consequence is felt by someone running it a day later. A fleet that keeps authenticating\n looks compatible and is not, if nothing in it can renew. Any automated check of this rule would\n read commit markers, so a break recorded as a feature is the one case it could not see, which is\n why the rule is written for people first. **The marker held for this release: the 0.49.0 change\n that caused all of this, `36d177951 feat(core)!`, did carry its `!`.** The rule exists for the\n next one that does not.\n\nWhat this page does not promise is a rolling upgrade. Nothing in the current line dual-serves two\nauthority versions, so where broker and manager run separately there is a window in which the mesh\nis down. The sections below give that window's shape so it can be scheduled rather than endured.\n\n## Personal native connections in 0.92.0\n\nUpdate the CLI and its Claude plugin together, then reopen the native Code session. Existing static\nauth connections keep their credentials and gateway tools. Personal signed-in connections are new;\nthe plugin binds a native conversation through its ordinary command hooks before creating one.\nThis works without intercepting the `classic.*` events protected by Claude's built-in guard; keep\nthe guard enabled. Organization policies that disable command hooks or native modules still apply.\nRemote meshes must advertise\nconversation identity v1 on the selected registration's HTTPS origin. The existing account signs\nin through Cotal; do not copy login files or paste a token into Claude.\n\nEach signed-in conversation gets its own roleless actor, direct messages and an own-event-only\npublication grant. The space's required-event policy remains in force. Desktop Chat cannot use this\nnative path because it has no Code transcript binding.\n\nCreation persists the request and secret before contacting the authority. Retrying an interrupted\ncreation reuses them. Disconnect can report pending cleanup; retry it for the same connection.\nPrivate recovery state remains until the authority confirms that exact lifecycle is retired.\nNeither an upgrade nor a lost response silently creates replacement authority.\n\n## Cooperative workflow cancellation in 0.92.0\n\nThe manager adds the issued, instance-bound `run-cancel` command. Its cluster revision advances to\n37, retaining the vitals and keyed-submission commands. Cancellation is available through the\ngeneric endpoint client; this change adds no CLI or MCP cancellation shortcut.\n\n### What keeps working\n\nExisting run records, admissions, revocation markers and journals keep their formats. Upgrade does\nnot rewrite them, cancel a run or remove a marker. A released run without a marker can still resume\nunder its admission. A revoked run remains revoked. Manager shutdown still requests boundary\nrelease; it does not write a cancellation marker or prove an agent process stopped.\n\n### What needs an update\n\n- Custom `RunHost` implementations must return a drive with `cancel(reason)`, in addition to\n `done` and `release(reason)`. It must propagate cooperative root cancellation to in-flight\n effects. Do not substitute boundary release or process termination. Rebuild and qualify the\n custom host against the new core contract before enabling it on an upgraded manager.\n- The built-in runtime and language packages carry the root signal through both engines and the\n compiled worker. Keep core, auth, manager, runtime and language on the same release; copying one\n source file or replacing only the manager does not supply the new protocol and worker code.\n- Existing caller credentials do not gain a new publish permission on installation. Obtain fresh\n issued credentials through the supported issuer before using `run-cancel`. Static credentials\n and caller generations must still meet the original lifecycle and grant checks. Never edit a\n credential to add the permission.\n- The new command requires a fresh Describe-derived instance and epoch bind. An old issuer lacks\n its independent request observer and inspect/commit path, so a new signerless manager cannot use\n that issuer for cancellation. Do not treat missing support as a reason to bypass the issuer.\n\n### Order for a split topology\n\nSchedule a maintenance window for the version change; this is not a rolling-compatibility promise.\nTake the deployment's usual consistent backup of broker state, including run records, admissions\nand journals, and the issuer's private configuration. Upgrade the issuer, then the manager and its\nruntime dependencies to the same release, using their normal stop/start procedure. Reinstalling a\npackage does not replace modules already loaded by a running process. Reconnect callers through\nthe upgraded issuer and resolve the manager's current contract before invoking cancellation.\nThere is no broker format migration for this command and no automatic cancellation during upgrade.\n\n### What the result establishes\n\nA durable marker denies future admission but is not proof of cessation. Only `stopped` with the\ncaptured drive outcome establishes that local drive's result. If the holder recheck fails or the\nrequest budget expires, a parked effect can remain running beside a committed marker. Preserve an\nunknown result; do not blindly resend it or declare cleanup complete. To continue cancellation,\na caller can make a fresh explicit request, which observes the marker and current holder before\nreporting its own result. Pending cancellation intent can also block a local launch after a definite\nno-dispatch refusal if marker absence was not confirmed within the budget.\n\n`run show` now displays revocation independently of recorded state. An unreadable marker yields\na partial result and exit 4 in human and JSON output, rather than an apparently complete view.\nAutomation must handle that existing partial-result convention. None of these observations proves\nmodel or tool preemption, physical process cleanup or cessation on another host.\n\n## Service authority status selector in 0.92.0\n\nA service endpoint authority status query that reads one operation now names that operation:\n`target: { kind: \"operation\", operation, requestId }`, where `operation` is `register`,\n`activate`, `renew`, `ready` or `revoke`. `@cotal-ai/core` exports the operation names as\n`ServiceStatusOperation` and the whole selector as `ServiceStatusTarget`. The 0.91.0 selector,\n`{ kind: \"operation\", requestId }`, no longer parses. The change is in `parseServiceAuthorityRequest`,\n`parseServiceAuthorityEnvelope` and the `ServiceAuthorityRequest` type, so it reaches every client\nand server built on them.\n\nA `revoke` operation status parses, but the 0.92.0 auth service does not answer it yet: it is\n`denied` with reason `unsupported` (HTTP 422), as SPEC states. To follow a revocation, read\n`target: { kind: \"instance\" }`, which reports `revoking` with the revoke's operation id.\n\n### What keeps working\n\n- Every running mesh. 0.91.0 shipped the request types and the client (the auth provider's\n `serviceEndpointAuthority` hook and its HTTP client), but no 0.91.0 auth service serves\n `/service-endpoint-authority`, so no deployment has status traffic on this route to lose. In\n 0.92.0 only a context opened with `serviceEndpoints` serves the route. That option is off by\n default and has no CLI flag; without it the route answers 404.\n- `target: { kind: \"instance\" }` is unchanged, and so are the register, activate, renew, ready and\n revoke requests themselves.\n- No credential, broker or connection is involved. Nothing needs reminting and no broker restarts.\n\n### What does not migrate\n\n- **Code that builds an operation status query without `operation`.** The 0.92.0 provider hook and\n client parse each request before sending it, so the query fails locally with a `bad-request`\n error, `service-endpoint authority: <detail>`, whose detail is\n `object has missing or unsupported fields`, and nothing is sent. An operation name outside the\n five gets the detail `unsupported status operation`.\n- **A 0.91.0 client still sends the old selector.** A 0.92.0 auth service opened with\n `serviceEndpoints` answers it with HTTP 400, whose body carries the same parser message. The\n service reads and writes nothing for it and logs nothing. The 0.91.0 provider hook and client\n never show that message: they cannot parse the 400 body as a protocol result, so they throw a\n `ServiceAuthorityTransportError` whose message is\n `service authority returned an unconfirmed response; retain the existing request identity`, with\n `httpStatus` 400 and outcome `not-executed`. A status query is a read, so nothing ran and\n nothing needs replaying. An auth service without `serviceEndpoints` answers 404 before it reads\n the body.\n- **No legacy operation is inferred.** Neither the client nor the service maps an old selector onto\n `register` or any other operation. One requestId can key a different request under each\n operation, so a guessed operation could answer with another request's status. Send the operation\n the request was made as; the requestId you stored with it stays the same.\n\n### Durable data\n\nNothing stored changes on upgrade, and nothing is rewritten. The selector is a request shape and is\nnever stored. 0.91.0 wrote no service authority request intents or operation records, so there is\nnothing to convert. A 0.92.0 context opened with `serviceEndpoints` writes its own records from the\nmoment it is enabled: an admission record when it first opens, then request intents, the\nper-instance intent index, operation, proposal and assignment records, and endpoint gates. Each\nintent already records its operation. Without `serviceEndpoints` it writes none of them.\n\n### Order for a split topology\n\nUpgrade every client that builds status queries together with, or before, the first auth service\nyou open with `serviceEndpoints`. That means code using `ServiceAuthorityRequest` or\n`parseServiceAuthorityRequest`, and every process that calls the auth provider's\n`serviceEndpointAuthority`. This change adds no ordering for brokers, managers or connectors;\norder those as their own sections say.\n\nThere is no compatibility window to schedule. A 0.92.0 auth service never accepts the old selector,\nand a 0.91.0 auth service has no handler for the route at all, so a mixed pool is not partly\nworking: every 0.92.0 auth service opened with `serviceEndpoints` refuses an old client's\noperation status queries with 400, which that client reports as the unconfirmed-response error\nabove, and one without `serviceEndpoints` answers 404. Nothing else is interrupted, so the change\nhas no outage window of its own.\n\n### Before the upgrade\n\n- Nothing needs a snapshot for this change, because it writes nothing. If the same upgrade first\n enables `serviceEndpoints`, take the deployment's usual consistent backup of broker JetStream\n state and the auth host's private configuration before that step, because the service endpoint\n host then starts writing to the auth bucket.\n- Find the old selectors in your code, then name the operation in each:\n\n```sh\ngrep -rn 'kind: \"operation\"' --include='*.ts' --include='*.js' src/\n```\n\n```ts\n// 0.91.0\n{ ...coordinates, operation: \"status\", target: { kind: \"operation\", requestId } }\n// 0.92.0\n{ ...coordinates, operation: \"status\", target: { kind: \"operation\", operation: \"register\", requestId } }\n```\n\nWhat this section measured, and what it did not: the local refusal and the accepted and refused\nselectors are covered by the core parser's own test. The 400, 404 and `unsupported` answers, and\nthe absence of a log line, are read from the auth service's source. The 0.91.0 client's error is\nread from the 0.91.0 client and core parser source; the client file is unchanged since 0.91.0. No\nmixed-version deployment was run.\n\n## History reader deletes after 0.86.0\n\nObserver and admin credentials now carry a `CONSUMER.DELETE` row for each of the connection's eight\nhistory-reader names, `hist_<readerId>_<n>`, on CHAT (and on DM for admin). `channelHistory`,\n`multiChannelHistory` and `dmHistory` read under those names and delete each reader when its step\nends. Before this, every read left its consumers for five minutes, and a dashboard polling history\nfilled the stream's consumer limit until reads were refused with \"maximum consumers limit reached\".\n\n### What stops working\n\nA static observer or admin credential minted by 0.86.0 or earlier has no reader-delete rows. On it,\na current client's history read fails with \"it predates the observer and admin reader-delete grant,\nso remint it\", and leaves one reader per stream until the broker's five-minute inactive threshold.\nA credential the CLI mints at start (`cotal console`, `cotal web` on a static auth mesh) is current\nafter the upgrade.\n\n### Before the upgrade\n\n- Remint every observer or admin credential a process reads from a file: `cotal auth mint <name> --profile\n observer` or `--profile admin`, and any `--creds` file given to a dashboard.\n- On a user-auth mesh, the auth service mints these views per connection. Upgrade it before, or with,\n the hosts that read history, so no current client connects through an older auth service.\n\n## Native key issuance recording after 0.85.0\n\nBefore upgrading, take a consistent backup of broker JetStream state and the auth host's private\nconfiguration and signing material through the deployment's supported backup procedure. The auth\nbucket must use file storage. Allow space for one issuance record per new callout exchange until\nits minted expiry plus clock tolerance; static credentials have no such record.\n\nEmbedders that call `startAuthCallout` must supply the required `recordIssuance` callback. It must\nfinish a durable create-only write for the broker-generated user key and recheck the validated\nlifecycle and credential authority before returning. A missing callback refuses startup; a failed\nwrite or recheck denies the exchange. Do not use the isolated tests' no-op callbacks in a deployment.\nThe stock `startAuthService` authority plane wires the recorder and its pruning automatically.\nExisting auth records need no manual rewrite, and no tenant or participant grant is added.\n\nFor split deployments, pause new managed launches and stop every old auth-callout responder before\nstarting the matched new auth service. A mixed responder pool can still issue keys without records.\nNew connections and reconnects cannot authenticate while the callout service is down. Existing\nbroker connections keep their current JWT expiry; this change needs no broker restart. Upgrade the\nCLI, Manager and connector packages together before admitting new managed launches, using the\nruntime's supported Manager custody or recovery path. This source change does not promise that\nrunning seats survive a Manager restart.\n\nThe new service prunes expired issuance rows before opening and on a timer. A corrupt row refuses\nopening or fences the running authority plane; investigate the stored evidence through the normal\noperator recovery procedure. Do not erase the auth bucket or recreate signing keys to clear it.\nRows written before a later authority-check failure can remain until pruning; they do not prove\nthat a client received a JWT or established a connection.\n\nConnections admitted before the last old responder stopped, static JWTs and old responders have no\nissuance record. They are not backfilled or identified from names. Bearers without current lifecycle\nand credential claims cannot be recorded and are refused; use the supported enrollment or renewal\npath for the selected release. These records alone do not provide persistent native-key cutoff or\ncomplete revocation. Reintroducing an old responder also reopens the recording gap.\n\n## Native Claude MCP discovery after 0.82.2\n\nBefore upgrading, snapshot Cotal configuration and the Claude profile on each launch host.\nOrdinary Claude spawns now load the host's native MCP sources alongside declared Cotal overrides.\nAn empty sharing map no longer suppresses native servers. Audit enabled servers and their\ncredentials before admitting new seats. Use an explicit sharing selection, including\n`--share-tools none`, to retain isolation. Existing sessions keep their loaded configuration.\n\nNo broker restart, credential-format migration or cross-host profile copy is required. Upgrade\nthe CLI, core, manager and connector together on each launch host. Pause new managed spawns\nwhile adopting the matched packages; use the runtime's supported manager custody or recovery\npath. Any interruption is that manager transition, not a change to the broker wire contract.\nCarried private homes stay isolated and do not import the operator's native profile.\n\n## Connector-store protection after 0.82.2\n\nSnapshot the private Cotal state, connector store and mesh configuration before upgrading.\nInstall the new CLI on each CLI and manager host, then verify the selected executable before\nrunning setup or store maintenance. The POSIX installer prints its absolute launcher because\nit cannot change the parent shell's command cache, aliases or functions. An older executable\nalready on disk remains unsafe for a newer store; do not use its force advice to recover.\n\nForward setup and connector seeding migrate built-ins automatically. Same-generation force and\nreset rebuilds remain supported. An executable older than the store now refuses every backward\nmigration, including force and reset. Select a verified executable at least as new as the store\nand run normal seeding instead. No credential or mesh-record format needs manual migration.\n\nA split deployment needs no broker restart for this change. Keep each host's CLI and connectors\non the same release. Pause new managed spawns while adopting it; a running manager keeps its\nloaded code until restarted. Use the chosen runtime's supported custody or preserve-state path.\nThe interruption covers that manager transition and depends on its runtime, not a broker outage.\nDo not overwrite a live manager installation or stop an unidentified listener to force adoption.\n\nAn implicit foreground spawn now chooses a numbered actor when the requested actor is occupied.\nExisting grants, launch material and sessions stay in place. An explicit name remains pinned\nand refuses an occupied actor rather than renaming it. Signed enrollments and managed handoffs\nkeep their existing identity semantics.\n\n## Background startup in 0.91.0\n\n`cotal up` now returns after background startup by default. `cotal up --detach` retains that\nmeaning. Use `cotal up --foreground` when terminal lifetime should own the native stack.\n\nA systemd unit, launchd KeepAlive job, container entrypoint or script that supervises the `up`\nprocess must add `--foreground` before upgrading.\n[Persisted callers](#persisted-callers-in-0910) gives the literal\nreplacement for each.\n\nA project folder an earlier release started keeps its own launcher after the upgrade. Stop it with\n`cotal down` as usual, upgrade, then run plain `cotal up` in that folder: it starts on the retained\nlauncher with the same identity, store and history. It does not need `space create` or `space add`.\n\nRecorded recovery requests retain their original foreground/background intent. They do not\nreinterpret a prior request using the new default. This change does not migrate a whole stack to\nowned services.\n\nPrepared plain-loopback user-auth roots can use the controller's broker, auth service, delivery and\nmanager members. First enable may return a known partial result after the first three start: the\nmanager needs an existing signed-in, granted CLI login. Sign in and obtain that grant, then run\nbare `cotal up`; no static caller or new grant is substituted. Auth endpoints come from the\ncreation-pinned child's native provider readiness, not a guessed URL. The controller keeps unknown\noperations blocked. Existing three-member auth/open journals are read with an unattempted auth row\nand written with all four roles; old clients do not understand that new record shape. Upgrade the\ncomposed CLI together before using it. TLS-required, remote and public-exchange user-auth paths\nretain their native route.\n\n## Persisted callers in 0.91.0\n\nSome command lines outlive the install that wrote them: OS units, scripts and container commands.\nAfter `npm i -g cotal-ai@<new>` they reach the new binary before any `cotal` command has run. This\nsection says what each one does after the upgrade and what to change before it.\n\n### Command mapping\n\n| Earlier command | Command in 0.91.0 |\n|---|---|\n| `meshes`, `meshes add` | `space`, `space add <name>` |\n| `clean store`, `clean all` | `reset store <space>`, `reset space <space>` |\n| `mint <name>` | `auth mint <name>` |\n| `mint --signer` | `auth signer --out <private-file>` |\n| `actor list`, `actor grant <actor>`, `actor revoke <actor>` | `auth grants`, `auth grant <actor>`, `auth revoke <actor>` |\n| `join [channel]` | `chat [channel]` |\n| `console` | `watch` |\n| `channels` | `channel` |\n| `send dm <agent>`, `send msg <channel>`, `send ask <role>` | `send -t <agent>`, `send -c <channel>`, `send -r <role>` |\n| `personas` | `persona` |\n| `endpoints`, `describe`, `invoke` | `endpoint`, `endpoint describe <target>`, `endpoint invoke <target> <command>` |\n| `deregister-instance` | `endpoint deregister <class> <instance-id>` |\n| `reconcile-gate`, `seats` | `manager reconcile`, `manager seats` |\n| `topology view` | `team check <file> --graph` |\n| `down -f <manifest>` | `team remove <deployment-id>` |\n| `status --components` | `service` |\n| `run journal`, `run migrate`, `run ps` | `run show`, `run check`, `run` |\n| `deliver pending <name>` | `pending <agent>` |\n| `web` | `service add <name> --preset web`, then `service start <name>` |\n| The four daemon entries | Their service presets, under [deprecated daemon entries](#deprecated-daemon-entries) |\n\nThe sections on this page for each command family give the details of its change.\n\n### What stops working\n\n`cotal up` returns after background startup by default. An earlier line such as\n`cotal up --space <space> --server <url>` that a supervisor runs as its main process now exits\nonce the stack is started. systemd then treats a `Type=simple` unit as finished and can stop the\nprocesses that unit started, and a restart policy starts another attempt. A launchd `KeepAlive`\njob relaunches it the same way. Add `--foreground` before upgrading. For a systemd unit, replace\n`ExecStart=/usr/bin/cotal up --space <space> --server <url>` with:\n\n```ini\n[Service]\nType=simple\nExecStart=/usr/bin/cotal up --foreground --space <space> --server <url>\n```\n\nFor a launchd job, add `--foreground` after `up` in `ProgramArguments`:\n\n```xml\n<key>ProgramArguments</key>\n<array>\n <string>/usr/local/bin/cotal</string>\n <string>up</string>\n <string>--foreground</string>\n <string>--space</string>\n <string>main</string>\n <string>--server</string>\n <string>nats://127.0.0.1:4222</string>\n</array>\n```\n\nA container command or script makes the same change to its `up` line. When background `up` is the\nmain process of a systemd unit, it prints a warning naming the unit and the `--foreground` remedy\nafter it starts or refreshes the stack. `up -f`, which has no foreground form, and a dry run print\nnothing. A launchd job gets no warning. `cotal up --detach` keeps its meaning. The warning changes\nno mode and no unit, and no unit or script is rewritten or restarted.\n\n`cotal spawn <name>` now launches through a manager and attaches. A container or script that ran\nan agent directly with `spawn <name>` adds `--foreground`.\n\n### Deprecated daemon entries\n\n`supervise`, `deliver`, `auth-service` and `feedback-intake` stay accepted with their existing\nflags for every caller: units Cotal installed, units and scripts you wrote, and container commands.\nThey are deprecated and are removed in `1.0.0`, one release for all four. Help and completion no\nlonger list them. Each run prints one line on stderr naming its replacement and that release.\nStdout, behavior and exit codes are unchanged, so no unit restarts in a loop because of the\nupgrade. `deliver pending` warns the same way and names `cotal pending <agent>`.\n\n| Entry | Replacement |\n|---|---|\n| `supervise` | `cotal service add <name> --preset manager` |\n| `deliver` | `cotal service add <name> --preset delivery` |\n| `auth-service` | `cotal service add <name> --preset auth-service` |\n| `feedback-intake` | `cotal service add <name> --preset feedback-intake` |\n\n`cotal service add <name> --preset <preset> --help` lists the options a preset takes, and\n`cotal service start <name>` starts it. A `supervise --roster <file>` line maps to the manager\npreset's `--roster <file>`, with an absolute path, and `cotal service start <name> --foreground`\nkeeps the manager in the foreground as `supervise` did. The manager, delivery daemon and auth service that `up`\nstarts itself do not warn, so the line in a log always comes from a unit, script or container you\ncan change.\n\n### Owned units\n\nA manager unit that an earlier `cotal service install` wrote keeps calling `supervise`. After the\nupgrade it starts the new binary and prints the deprecation warning on each start. This release\ndoes not rewrite that unit.\n\nA unit that this release's `cotal service install <name>` writes runs the hidden `__service-unit`\nentry, which an earlier binary does not have. Before you downgrade, stop it with\n`cotal service stop <name>` and remove it with `cotal service uninstall <name>`. After the\ndowngrade, install the unit again with the earlier binary's `service install`.\n\n### Containers\n\nThe `solo` service in `deploy/docker/compose.yaml` runs `spawn scout --foreground`. The team\nservice and the image's default command still run `supervise` with `--roster`, so their logs show\nthe deprecation warning. An image you already built carries its own CLI and is unaffected until\nyou rebuild it.\n\n### Before the upgrade\n\n- Add `--foreground` to every unit, launchd job, container command or script that supervises\n `cotal up`.\n- Add `--foreground` to every container command or script that runs `cotal spawn <name>` directly.\n- Move units and scripts that call `supervise`, `deliver`, `auth-service` or `feedback-intake` to\n their service presets before `1.0.0`.\n- Before a downgrade, stop and uninstall every unit this release's `service install` wrote.\n\n## Team commands in 0.91.0\n\nThe new grammar names fresh intent `team boot <file>` and additive intent\n`team deploy <file>`. `team check <file> --graph` resolves declared access\nwithout a broker. `team` and `team show <id>` read local fresh summaries, additive ledgers and\nagent/channel returned receipts; `team remove <full-id>` is the ID-only additive\nteardown name. An edited manifest is not authority to remove an old deployment.\n\nBoot's `--dry-run` is a declaration view.\nDeploy's `--dry-run` reads native presence and channel registry for advisory\nclassification against a prior ledger. Remove's preview compares a legacy\nledger with native manager PS and display-only membership observations, or a\nreturned channel-only receipt with current KV revisions. No preview authorizes\na launch or stop. Bare `team boot <file>` enters guarded native fresh\nstartup only for a prepared, stopped local root; it refuses a leftover manager\nrecord and offers rich, JSON and plain receipts. User-auth boot records an intent before\nowner/trust preparation and retains uncertainty without retry. Windows boot\napply refuses, because protected service-record inventory is not supported there.\nBare `team deploy` applies all-new channel-only manifests with explicit live\ndelivery, or one to 100 all-new agents on a static/open mesh, optionally with\nup to 100 new live channel cards. A mixed deployment records one intent before\ncreating channels and launching agents through one pinned manager/runtime.\nEach channel revision and original launch request, allocation and matched join\nis recorded before continuing. A full receipt requires both sequences to match.\nUnknown effects are retained, not replayed; completed effects are not rolled back.\nBoth routes offer rich, JSON and plain output. Identical settled deployments can return a verified\nno-op without new effects. Named stale-agent waivers retain the old policy without restarting.\nUser-auth agent apply and existing-card mutation still refuse effects.\n\n`team remove <id>` can retire a returned static agent deployment through its original manager.\nReview `--dry-run` first; scripts require `--yes`. This needs the native slot query, original UIDs\nand native retired slot/head plus cleanup evidence, not just stop acknowledgements. A returned\nchannel-only deployment instead uses deletes conditioned on its original card revisions. A static\nmixed deployment retires agents first and needs a complete retirement receipt before checking and\ndeleting cards, under one confirmation and maintenance lock. Both receipts must exist before its\nblocker is released. Partial and unknown effects retain their journals without replay. History,\ngrants and readers are unchanged. Open/user-auth agent retirement and legacy removal still refuse.\nThe old `spawn -f` manifest spelling no longer deploys. Native `up -f` still\nstarts a fresh mesh; `down -f <file> --run <id>` remains the retained route\nonly for an older additive ledger. Move an automated agent deploy or teardown\nto the new spelling only when its auth mode and manifest are ones these routes accept.\n\n## Credential commands in 0.91.0\n\nCredential minting is `cotal auth mint <name>`. The old `cotal mint` spelling refuses before\nissuance and names its replacement. A stripped account signer is exported with the explicit\n`cotal auth signer --out <private-file>`, not `mint --signer`.\n\nThe default standing credential label is create-only. Repeated minting does not silently replace\nit. Choose a new label or a separate private output. An explicit existing output is replaced only\nwhen it is an owned private native credential matching the supplied `--identity`. Explicit\nexports cannot overwrite managed `.cotal` or workstation state.\n\nThe new receipt reports the actual principal, resulting lifecycle, private destination and\nnative expiry without credentials. Reusing an nkey does not promise reuse of its old lifecycle\nor bind-only inboxes. An agent-profile re-mint reports its newly allocated UID and warns about\nthat distinction; consuming clients must use the returned UID. Provisioning is explicit and is\nnot evidence that a client joined. Scripts should parse the finite `--json` document rather than\nscraping the former text lines. Secret and signer files must stay out of support bundles.\n\n`auth mint <name> --endpoint <class> --out <private-file>` is a separate caller mode. It uses the\nsame native static authority and a live digest-verified untargeted synchronous ephemeral surface.\nTargeted, journaled or action commands refuse the whole issuance; no unsupported row is dropped.\nThe native agent footprint and exact additional endpoint rows are provisioned with required TLS,\nwithout adding spawn/run/admin scope or starting a seat. `--channels` enters both explicit channel\nACLs and defaults empty; `--expires-in` remains native seconds.\n\nThe create-only private `<out>.issuance.json` intent is retained before footprint work and contains\npublic identity/lifecycle/scope metadata only. It is not a native grant or completion receipt. An\nexisting output or intent blocks replay, including when an interrupted operation left no output.\nKeep that intent with any original file while diagnosing an unknown result. Provisioner close is\nreported separately. No raw-file caller identity, managed launch, user-auth issuer or environment-\nexport onboarding is added by writing this credential.\n\n## Endpoint deregistration in 0.91.0\n\nReplace `deregister-instance --endpoint <class> --instance <id>` with\n`endpoint deregister <class> <full-instance-id>`. Both positional values are required.\nThe old spelling refuses before effects and has no implicit instance selection. Review the\nnew `--dry-run` output, then use `--yes` in scripts. JSON retains native revision receipts;\nunknown or superseded deletions are not retried. User-auth and open targets do not receive\na static-signing fallback.\n\n## DM pending inspection in 0.91.0\n\nUse `cotal pending <agent>` for a one-document JSON/plain read of native DM\ndurable counts and bounded candidate IDs. `--durable <exact-dm-durable>` targets\nan older lifecycle directly. The retained `deliver pending` spelling still\nuses its earlier output shape; scripts must migrate to `result` fields rather\nthan assuming the old per-line format. Candidate IDs are not proof of\nrecipient delivery or complete message history. This route has only the\nstatic/open admin read authority; user-auth has no corresponding view yet.\n\n## Live space deletion in 0.91.0\n\n`space forget <name>` changes only the local registration. It does not delete\nlive broker state. `space delete <name> --dry-run` now reads known first-party\nnative streams, bucket backing streams and presence entries on the registered\nbroker in open or static-auth mode. Static transfer-stream names are read under\nan additional exact-name grant on a second confirmed connection. This non-atomic\nread is not an apply plan or evidence that presence records are live. User-auth\ninspection and deletion remain unavailable. Bare deletion now requires a typed\nexact-name confirmation on a terminal or the exact positional name with `--yes`\nin a script. It rereads the native impact and checks the current target before\nsubmitting exact stream deletes, stopping on the first refused or unconfirmed\nreply with acknowledged and remaining names. A native post-read reports any\nsurvivor or newly observed transfer target without retrying. No process, registration or local\nfile is removed.\n\n## Stopped reset in 0.91.0\n\n`reset store <space>` and `reset space <space>` are the named replacements for\n`clean store` and `clean all`. `reset store --dry-run` provides a partial\nlocal-impact preview of the store path, recorded processes, maintenance journal\nand broker credless reachability. `reset store` requires confirmation,\nacquires the maintenance lock, rechecks its exact store identity, legacy\nprocesses and all recorded service instances for that space, and calls the\nexisting store-only cleanup. Any recorded supervisor blocks reset. It keeps trust and the\nregistration. TLS-required or TLS-first store targets refuse, because no trusted\nstopped-broker check covers them. `reset space` now routes a\nreviewed local identity reset through native clean-all. It requires Linux, an\nisolated locally started registration and plain-NATS loopback nonresponse.\nIts preview inventories provider-declared keys and native filesystem targets;\napply rechecks the scope under the maintenance lock and constrains each delete\nto that reviewed inventory. Trust is removed last, followed by the unchanged\neligible registration. Unknown effects are not replayed. Service definitions,\npersonas, packages, IdP logins and remote data stay. Unsupported providers,\nforeign material, TLS-first and non-Linux targets refuse.\n\n## Backup maintenance in 0.91.0\n\n`backup create` supports one-document `--json` and plain reports. The completion receipt follows\nnative attempt cleanup. A published artifact with unconfirmed cleanup is partial; retain its\npath and original attempt instead of repeating creation. `backup inspect` reads metadata and\n`backup verify` checks declared file sizes and hashes. Neither proves restoration or authority\ncontinuity, starts a broker or writes a staging tree.\n\nThe canonical stale pre-commit recovery is `backup recover <full-attempt-id>`. Healthy retained\nold-source cleanup is `backup cleanup-fallback <full-attempt-id>`. Review `--dry-run`, then use\n`--yes` in scripts. Neither flag bypasses owner, deadline, root, journal revision or store-identity\nchecks. Restore startup stays on its guarded existing route.\n\nA controller-owned stack cannot be preserved in 0.91.0. On a root with a stack journal,\n`down --preserve-state`, `backup create`, `backup restore` and `up --restore` refuse `UNSUPPORTED`\nbefore any effect. Stop such a stack with plain `cotal down`; it keeps its store, channels and\nhistory for the next `cotal up`. Preserve, backup and restore of a controller-owned stack return in\na later release. A root without a stack journal keeps its preserve, backup and restore.\n\n## Feedback input in 0.91.0\n\nUse a protected `feedback --key-file <path>` rather than placing an intake key on argv.\nThe old `--key` spelling refuses without sending or echoing its value. Public feedback still\nworks with a contact email and requires no environment setup. JSON/plain output is one public\nreceipt, not a copy of the submitted body. Unknown submission outcomes are not retried, and no\nredirect or alternate credential/intake is substituted.\n\n## Credential diagnosis in 0.91.0\n\n`doctor auth` uses JSON/plain reports and distinguishes local claims from live authorization.\nA plain diagnostic that encounters material requiring migration refuses without moving it.\nExplicit `--fix` retains the native static daemon repair, with separate migration/re-sign receipts\nand no user-auth signer fallback. A local re-sign is not daemon adoption or broker acceptance.\nUnhealthy, partial and unknown results retain their classified outcomes; scripts must read the\nnew result and error fields instead of scraping the old `auth: healthy` line. Never respawn from\na locally ordered older credential file alone.\n\n## Service adapter declarations in 0.91.0\n\nA local-process provider can expose the version-1 workstation service ABI alongside its existing\nprocess metadata. It declares a closed option schema, explicit foreground/multi-instance/log\ncapabilities and launch/readiness hooks. Re-add an extension after adding this ABI so its cached\nservice metadata and package fingerprints are recorded. An older descriptor is not imported to\nguess its schema. Plain process definitions keep their existing meaning.\n\nPresets are declarative registry contributions. Their option flags, help, completion and guided\nquestions derive from the adapter schema. Re-add a preset provider to populate its new cache;\nmissing or changed metadata refuses without importing code to guess the definition.\n\nThe top-level `feedback-intake` launcher is a [deprecated entry](#deprecated-daemon-entries). For a\nnew intake, save a definition with\n`service add <name> --preset feedback-intake --space <s> --root <absolute-root> --keys <private-keys.json> --creds <private-creds>`, then use `service start <name>` in that space. An explicit\n`--store` must be an existing private file. The default is private and instance-keyed. No broker\nor other service is started, no signer is minted and required TLS is preserved. Native startup\nreceipts are private, request-specific observations rather than native endpoint serving claims.\n\nA service instance pins its adapter name, package version and declaration fingerprint. A later\npackage update does not silently adopt a running or retained instance under a different adapter.\nStop the exact owned process and explicitly remove its stopped records before choosing changed\nadapter metadata. Logs remain private local files. Extension removal refuses live, foreign or\nunresolved service instances, including records outside the current mesh selection. None of\nthese declarations migrate legacy PID records or supply OS-unit ownership. Native manager\nimpact comes from its explicit adapter's attributed current read, not cached metadata.\n\nThe delivery implementation now registers its service adapter and legacy custody descriptor.\nCustom composition roots that want those inventory/control rows import `@cotal-ai/delivery`.\nThe full binary already composes it. CLI stop helpers share the workspace descriptor without an\nimplementation-to-implementation import. The `delivery` preset requires the recorded root and\nexisting scoped credential, keeps the native store/account/lease guards and does not adopt an\nexisting legacy daemon. Its hidden child entry is recognized by the same process-role predicate.\nThe old `deliver` machine-facing route remains for the stack callers that still use it.\n\nFresh static-auth and open `team boot` now use controller-owned stack members and accept\n`--foreground`. Their manager receives a digest-pinned manifest launch spec, with no additive\nownership ledger. A recorded stack can resume only after a completed whole-stack stop with its\nmanager never attempted, recorded broker inputs and each member's settled native evidence. Legacy\ncustody, prior team boots, supervisors and unresolved attempts still refuse. User-auth preparation\nuses ordinary `up --user-auth`, login and an explicit grant, then `down` before `team boot`. No\ncaller or owner grant is substituted.\n\nNew controller journals record the broker store path, storage cap request and bind host. A later\npreparation keeps those inputs and refuses differing explicit values. Older journals remain\nreadable. If their broker is stopped, restart requires the original store named with `--store-dir`\nand the original `--max-file-store` and `--host` values, if set. The store must already be a real\ndirectory. Do not omit the cap or host unless the original start set neither. That restart records\nthe inputs; run `down` before the stopped-stack team route. No store path is inferred from an\nabsent field, and bare restart never silently opens a default empty store.\n\nThe native manager status contract adds optional caller-scoped stop-impact metadata and advances\nthe cluster revision. Upgrade the composed manager and its compatible registration host together;\na host that does not accept the current contract must refuse registration. Older managers can\nomit the observation. Component status then says impact was not reported, rather than inferring\nthat seats survive. An observed policy does not migrate PID records, reserve a stop or confirm\ncleanup. The manager package adds a separate controller-only `manager-service` adapter and\n`manager` preset. Existing legacy manager records are not migrated or adopted. Update the\ncomposed packages and extension metadata together; an old cached declaration is not rewritten\nbehind a retained instance. Native stops require a fresh full-scope read and unchanged request-bound\npolicy, with native returned evidence plus original process exit. Missing terminal evidence retains\nits reservation and blocks replacement, without force kill. Cleanup remains not checked.\nThe direct service route currently refuses foreground mode. On an open mesh there is no\nauthenticated caller, so readiness and stop impact come from the pinned child's own request-bound\nlocal receipt. Its registration must equal the launch identity bound to that PID. Status shows it\nas a child-reported local receipt, not an attributed endpoint response. OS installation and\nnon-Linux protected process support are not available.\nRecorded systemd user units with an original invocation ID use that invocation's native journal\nfor finite/follow logs. Missing invocation evidence refuses instead of reading a stale direct-child\nfile. Native cursors and timestamps are retained; journal retention/access is not proof of complete\noutput or completed service work. No unit install/control support is implied by this read route.\n\nAuth providers may implement the optional read-only `preflightServer` hook. It checks public\nserver inputs without creating a directory, pin, key, grant or daemon. The controller's user-auth\nroute requires it before preparation; providers without it refuse rather than skipping validation.\n`prepareServer` remains authoritative and checks again before provisioning.\n\nThe auth package also declares an `auth-service` preset over its prepared native key projection.\nIt does not migrate or reclaim existing PID records. Its real child claims custody before opening\nplanes and publishes a private startup phase only after native initialization and signal handlers.\nThis route declares shared foreground control and still refuses remote broker and TLS-required\ntargets. The transport refusal is\nintentional, because not every native authority connection carries the registered transport\nrequirement. No plaintext substitution or new issuer is used.\n\nManager maintenance now supports finite JSON/plain reports. Gate repair and seat drain default\nto cancellation in a terminal and require `--yes` in scripts. Use `manager reconcile --dry-run`\nor `manager seats --drain --dry-run` first. Gate repair requires one registered root/space/broker\nand carries its TLS requirement through native liveness/eviction. It does not dial a default broker\nwhen that target is missing. An interrupted submitted repair or unresolved drain can return exit 3;\nconfirmed native results remain distinct from unconfirmed effects. No process is started.\n\n## Linear reports in 0.91.0\n\nUpdate the installed Linear extension with the matching CLI/workspace generation. Account add/show,\ninventory and direct call/resource/prompt JSON now use version-1 envelopes: read the native\ninventory from `result.inventory` and a native request reply from `result.reply`. Rich/plain reports\nbound displayed rows and lines without truncating JSON. MCP tool `isError` remains a result with\nexit 0 and a warning, not a work-success claim. Native not-executed refusals stay distinct from\nunknown submissions, and neither is replayed automatically.\n\nThe `linear` service preset runs the same native endpoint implementation from an explicit static-auth\nroot and existing account. It retains native registration, renewal and bounded deregistration; no\nnew generic caller is issued. Its private phase proves handler initialization only, not a current\nendpoint reply or completed deregistration. Foreground service start uses the shared terminal\nlifetime and native signal handlers, not the old serve parser. User-auth/open targets and\nOS-supervised service operation refuse. The older `linear serve` route remains available for\ninstalled callers.\n\nA bare Linear/account group opens no account. Wrong-mode flags or extra operands refuse before\naccount access. Interactive OAuth remains a human flow without JSON. Foreground serve stays on its native human-output path.\nStatic caller issuance keeps its native authority and now returns shared JSON/plain private-file\nreceipts. Use an existing protected physical parent outside managed state; an existing output\nrefuses before provisioning. Uncertain provisioning/publication is not replayed and no environment-\nexport launch recipe is printed. The supported `auth mint --endpoint` form resolves its scope live;\ntargeted/journaled/action issuance and consuming launch adapters are not supported. These changes do\nnot replace ownership, start a service or print secrets.\n\n## Setup inputs in 0.91.0\n\n`cotal setup` takes four inputs: destination, application, project folder and space. Nothing on a\nrunning mesh changes, and there is no outage window.\n\n### What stops working\n\n- A `cotal setup --yes` or a setup with no terminal exits 1 with `USAGE_INVALID` unless it names\n every input as a flag. It no longer sets up every detected connector and seeds the folder it\n runs in.\n- Setup sets up one application, the one `--harness` names, and records it as the space's\n `harness` preference. An application whose executable is not on PATH exits 4 with the rest\n saved; it used to be skipped with a warning.\n- The default persona lands in `<project>/.cotal/agents`. Setup no longer looks for a running mesh\n to seed into.\n- A repeat `cotal setup` no longer prints a status card. `cotal status` reports the same state.\n\n### Before the upgrade\n\nA script or image that ran `cotal setup --yes` names the four inputs:\n\n```bash\ncotal setup --destination local --harness claude --project <dir> --space <name> --no-input\n```\n\nFor a space that runs elsewhere, register it first with `cotal space add`, or pass the bundle its\noperator exported with `--connection-file`, and use `--destination self-hosted` with the\nregistration's root as `--project`.\n\nStartup remains opt-in. `--start` conflicts with `--configure-only`; a script, JSON or plain\ninvocation must add `--detach`. `--yes` by itself still saves configuration only. Startup failure\nleaves completed preparation in the result. Do not treat an unknown startup result as permission\nto submit another launch; inspect the selected space and recorded original request first.\n\n## Reads that install nothing in 0.91.0\n\nA read no longer installs or refreshes the built-in connectors and the web dashboard before it\nruns. Nothing on a running mesh changes, and there is no outage window.\n\n### What stops working\n\n- On a fresh machine, `cotal status`, `cotal ext list`, `cotal ps`, `cotal runtimes`, `cotal docs`\n and the other reads leave the extension root empty. `cotal setup`, or any command that is not a\n read, installs the built-ins as before.\n- After an upgrade, a read does not refresh the built-ins. Where it would load one still at the\n older version, such as a connector's setup row in `cotal status`, it names the package and its\n version with `cotal ext seed` instead of loading it.\n\n### Before the upgrade\n\nA script that ran a read to install the built-ins, such as `cotal ext list` right after\n`npm i -g cotal-ai`, runs `cotal ext seed` instead.\n\n## Space selection in 0.91.0\n\n`cotal use` and the target a command picks without `--space` change. Nothing on a running mesh\nchanges, and there is no outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal use <space>` no longer checks the broker. It selects any registered space, including a\n stopped one, and the command that targets it reports the space not up. A name that is not\n registered exits 2 with `TARGET_NOT_FOUND` instead of 1. Bare `cotal use` reports the selection\n instead of exiting 1 with its usage, and `--json` writes one envelope.\n- A command run without `--space` stays on the selected space when its broker is down and refuses\n with `TARGET_UNAVAILABLE`, exit 2. It used to fall back to the one other running mesh and print\n a note that the default mesh was down. A selection that names a space no longer registered now\n refuses with `TARGET_NOT_FOUND` instead of falling back to the local project or the one running\n mesh.\n- `cotal space` lists the registered spaces. `cotal meshes` still lists them, and its `--json`\n lines keep their shape.\n\n### Before the upgrade\n\nA script that relied on the fallback names the space it means with `--space`, or selects it with\n`cotal use <space>`. A script that read `cotal use` exiting 1 for a stopped space checks the\nbroker with `cotal space show <space> --json` and reads `result.check`.\n\n## Extension listing output in 0.91.0\n\n`cotal ext --json` and `cotal ext list --json` now write one JSON document instead of one JSON line\nper extension. Nothing on a running mesh changes, and there is no outage window. Upgrading the\n`cotal` binary is the whole operation.\n\n### What stops working\n\n- A script that reads `cotal ext list --json` line by line reads one envelope instead. The rows it\n read are now `result.items`, each with the same fields, and `result.root` is the install prefix.\n With `jq`, `cotal ext list --json | jq -c '.result.items[]'` prints the rows as before. An empty\n prefix writes an envelope with no items instead of nothing.\n- A usage error from `cotal ext` exits 1 as one `refused` envelope under `--json`, and `--plain` or\n `--json` on `ext add`, `ext remove`, `ext root` or `ext seed` is refused. `cotal ext root` still\n prints the path alone.\n- The human listing is a table now. Human output is not a parsing target; use `--json`.\n\n### Before the upgrade\n\nSearch scripts for `cotal ext list --json` and `cotal ext --json`, and change line-by-line reads to\nread `result.items` from the one document.\n\n## History reads in 0.91.0\n\n`cotal history [channel]` reads retained messages. Clearing is now `cotal history clear`, which asks\nin a terminal and takes `--yes` in a script.\nThis changes the CLI grammar only. Stored messages, credentials, agents and running services do not\nmigrate or restart, and there is no outage window. Upgrade the CLI and its first-party components\ntogether through the normal update path; a separate broker or manager can stay running.\n\n### What stops working\n\n- Replace a script's `cotal history --force` with `cotal history clear --yes`. `--force` is refused\n on every `history` form, `history clear` included, rather than clearing anything. Add `--dms` to\n the explicit clear command only when the purge should include direct messages and the identity\n holds that authority.\n- `history clear` accepts no channel argument, read filters or `--follow`. It purges every channel\n in the selected space; these inputs are refused rather than treated as a selective purge.\n- Bare `history` reads channels within the caller's read grant. It never clears stored messages.\n Use `--json` and `result.items` for scripts; the human report is not a parsing target.\n- A user-auth history read uses the connection's channel grant. DM audit history and stream-sequence\n cursors are not available through that ordinary human credential. Use a time with `--since`.\n\n### Before the upgrade\n\nFind scripts that pass `--force` to `history` and change their explicit purge invocation. No stored\nstate or credential backup is needed for this CLI-only transition. Keep the deployment's usual\nbackup procedure before an intentional history purge; reads do not make a backup or restore data\nthat retention has already removed.\n\n## Status output in 0.91.0\n\n`cotal status` writes one structured document with `--json`, takes `--plain` and `--all`, and exits by\nthe structured outcome table. Nothing on a running mesh changes, and there is no outage window.\nUpgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal status` exits 4 when it could not read part of what it reports, such as a PID record, the\n folder's trust material, a persona catalog, the mesh registry or one of its records, or a user-auth login. It exited\n 0. An unreachable broker, a stopped process\n or an unbound responder still exits 0.\n- The recorded meshes list checks only the selected mesh and reads `not checked` for the others. Pass\n `--all` to check every one.\n- The report no longer lists the roster or prints a `cotal status` title line, and its standalone\n hints moved to a `Hints` section at the end. Rows are labeled `name: value`. Human output is not a\n parsing target; use `--json`.\n- `--components` is refused with `--json`, `--plain` or `--all`. Its exit, `0` to `3` by component\n health, replaces the report's, so it does not exit 4 for a partial report.\n\n### Before the upgrade\n\nSearch scripts and monitors for `cotal status`. A check that treats any nonzero exit of bare\n`status` as an outage should treat 4 as a report with unreadable parts. A script that read the\nroster from `status` should use `cotal ps` for managed seats.\n\n## Read command output in 0.91.0\n\n`cotal ps --json` now writes one JSON document instead of one JSON line per seat, and `ps`, `models`\nand `runtimes` exit by the structured outcome table. Nothing on a running mesh changes, and there is\nno outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- A script that reads `cotal ps --json` line by line reads one envelope instead. The rows it read\n are now `result.items`, each still the manager's row unchanged, and `result.managers` names each\n manager instance and whether it answered. With `jq`, `cotal ps --json | jq -c '.result.items[]'`\n prints the rows as before. Instance headers no longer go to stderr under `--json`.\n- An incomplete manager census, where a registered manager did not answer, refused or serves a\n different contract, exits 4 instead of 1.\n- A mesh that `ps` or `models` cannot resolve or reach exits 2 instead of 1, and a space catalog\n refresh that `ps` or `models` needed and could not get exits 2 instead of 1. Under `--json` each is\n one `refused` envelope whose `error.code` names the reason.\n- `cotal models` exits 4 when a connector's catalog could not be read, and `cotal runtimes` exits 4\n when an installed runtime provider fails to load. Both exited 0. `cotal models --agent <name>`\n exits 5 when that one catalog could not be read. OpenCode 2.x, which has no catalog the CLI can\n list, is reported `supported: false` with the reason in `unavailable` and does not fail the read.\n- The human reports of `ps`, `models` and `runtimes` are tables now, and `ps --wide` prints one\n labeled record per seat instead of a facts line under each row. Human output is not a parsing\n target; use `--json`.\n\n### Before the upgrade\n\nSearch scripts for `cotal ps --json`, and for checks of the exit status of `ps`, `models` or\n`runtimes`. Change line-by-line reads of `ps --json` to read `result.items` from the one document.\n\n## Shell completion script in 0.91.0\n\n`cotal completion <shell>` is now `cotal completion script <shell>`, and bare `cotal completion`\nreports which shells have the stub instead of printing usage. Nothing on a running mesh changes, and\nthere is no outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\nA startup line written by hand that runs the old form, such as `source <(cotal completion bash)` or\nthe PowerShell profile line `cotal completion powershell | Out-String | Invoke-Expression`, now\nloads no completion. The command exits 1, prints nothing on stdout, so the shell sources nothing,\nand names the replacement on stderr. Replace the line with its new form:\n\n```bash\nsource <(cotal completion script bash) # bash\nsource <(cotal completion script zsh) # zsh\ncotal completion script fish | source # fish\ncotal completion script powershell | Out-String | Invoke-Expression # PowerShell $PROFILE\n```\n\nA cached stub from an earlier release keeps calling the hidden `__complete` entry. The new ownership-aware\ninstaller does not adopt it just because its text matches a generated script. Review unrecorded\nstubs and custom startup wiring manually before replacing them. Nothing automatically rewrites\na hand-edited startup file. On Linux and macOS, `completion install powershell` asks the\ninstalled shell for its current-user all-hosts profile before adding an owned block;\nit does not adopt an existing manual line. Windows profile editing still requires a\nmanual review and the printed script.\n\nNew installs record their stub hash and startup block. Reinstall and update refresh unchanged\nrecorded stubs at the same path. Update adds no startup block and does not restore a stub the user\nremoved. Uninstall removes only unchanged recorded material. Modified or unowned material is\npreserved with a diagnosis. Reopening the shell is necessary to stop using functions already\nloaded in its parent process.\n\n## CLI command resolution in 0.91.0\n\nThe CLI now resolves and validates a command before it seeds connectors or writes configuration.\nNothing on a running mesh changes. Brokers, managers, seats, credentials and mesh records need no\nmigration and no restart, and there is no outage window. Upgrading the `cotal` binary is the whole\noperation. What changes is what two kinds of command line do.\n\n### What stops working\n\n- `cotal -v` no longer prints the version. It exits 1 and names `cotal version` and `cotal -V`. A\n script that reads the version this way must use `cotal version`, `cotal --version` or `cotal -V`.\n- The words `sp`, `a`, `s`, `r`, `x` and `h` are now aliases of built-in commands (`spawn`,\n `attach`, `status`, `run`, `ext` and `history`). An installed extension that provides a command\n under one of those words still loads, but the built-in wins and every run prints a warning naming\n the extension. Remove or update that extension to clear it. `cotal ext add` refuses a new\n extension whose name or alias is already claimed.\n\n### Before the upgrade\n\nSearch scripts and shell aliases for `cotal -v`, and run `cotal ext list` to see whether an\ninstalled extension provides one of the words above. There is nothing to snapshot. The extension\nmanifest an older binary wrote is read as before.\n\n## Messaging commands in 0.91.0\n\n`cotal channels` is now `cotal channel`, and `cotal send` names its destination with a flag instead\nof a `dm`, `msg` or `ask` word. Nothing on a running mesh changes, and there is no outage window.\nUpgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal channels`, with or without `list`, `set` or `default`, exits 1 as an unknown command. Bare\n `cotal channel` lists the channels, `cotal channel set <name>` saves one channel's settings, and\n `cotal channel defaults` reads or updates the space-wide defaults.\n- `cotal send dm <agent> \"<text>\"`, `cotal send msg <channel> \"<text>\"` and\n `cotal send ask <role> \"<text>\"` exit 1 with `USAGE_INVALID`, because they name no destination\n flag. Use `cotal send -t <agent> \"<text>\"`, `cotal send -c <channel> \"<text>\"` and\n `cotal send -r <role> \"<text>\"`.\n\n### Before the upgrade\n\nSearch scripts and shell aliases for `cotal channels` and `cotal send`, and change each line to the\nnew form above.\n\n## Managed Claude workspace trust in 0.80.0\n\nBefore upgrading, snapshot the manager's private Cotal state and Claude configuration home.\nPause new managed spawns while upgrading the connector, core, manager and chosen terminal\nruntime together. For tmux, cmux, Herdr, Orca and custodial Linux PTY seats, use that runtime's\nsupported manager-reload path and observe its custody/release bounds. Default `auto` selects\nin-process `pty`, whose seats cannot survive a manager restart. Drain those seats at an approved\nidle window, or use the documented preserve-state cut and `up` recovery, which resumes them\nrather than keeping their processes running. This upgrade does not require a broker restart.\n\nManaged launches now accept the authorized workspace through Claude's native startup dialog. They\nno longer require a prior interactive visit to that directory. Claude retains its normal\nrepository and linked-worktree trust semantics, including any trust it persists for later\nsessions. Foreground launches still prompt. No tool-permission bypass flag is added, but\naccepting workspace trust enables that checkout's hooks and settings, including its default\npermission mode unless a launch flag overrides it. Authorize only reviewed workspaces.\n\nThe launch recipe includes optional startup choices before its final confirmation. A custom\nruntime must implement `LaunchSpec.confirmBefore`, or refuse a recipe carrying it. Ignoring\nit leaves an untrusted launch at the native dialog until startup times out. The new launch\nrecipe does not require restarting an existing seat; manager reload continuity remains\nruntime-dependent as described above. Adopt the new runtime before launching new seats.\nNo credentials, mesh records or operator configuration files need manual migration.\n\n## AG-UI `takeCodePoints` export in 0.79.0\n\n`@cotal-ai/connector-core` no longer exports `takeCodePoints`. The AG-UI preview splitter is its\nonly user, and the package exported it only so the emitter could shorten a `RUN_ERROR` failure\ndetail, a path that never ran and is now removed. Nothing about a running mesh changes, and no\nshipped connector or host imports it. Only code of your own that imports it from the package is\naffected.\n\n### What stops working\n\nAn import of `takeCodePoints` from `@cotal-ai/connector-core` no longer compiles, and an ES module\nthat names it fails to link with `does not provide an export named 'takeCodePoints'`.\n\n### Before the upgrade\n\nTake a string's first `n` code points with `Array.from(s).slice(0, n).join(\"\")`, which is what the\nhelper did.\n\n## SecretStore create-only write in 0.78.0\n\n`SecretStore` from `@cotal-ai/core` has a fourth method, `create(key, value)`. It stores the value\nonly while the key is absent and resolves `true`, or resolves `false` and leaves the stored value in\nplace. The auth secret kinds made once per space now write their first value through it: the callout\naccount, the issuer keys, the owner secret and a hosted auth plane's instance identity. Before, two\nfirst calls on one space each made a value, the later `put` replaced the earlier one, and the earlier\ncaller kept using a value the store no longer held. The filesystem store implements `create` with an\nexclusive create, so a CLI root needs nothing, and stored values do not change.\n\n### What stops working\n\nA store of your own without `create` no longer compiles as a `SecretStore`. Plain JavaScript gets a\n`TypeError` at the first call that makes one of those kinds, such as `prepareServer` on a new space\nor the first `startAuthService` of a hosted context.\n\n### Before the upgrade\n\nAdd `create` to your store, decided in one atomic step by the backend, such as a conditional create\nor an insert that fails on an existing key. A read followed by a write brings the race back.\n\n## Manager-service authority refusal budgets in 0.78.0\n\n`POST /manager-service-authority` now counts its refusals against the exchange face's budgets, as\n`POST /exchange` does. On the loopback face a missing or wrong capability counts in the\ninvalid-capability window, and a refused IdP token or request counts in the refused-exchange window.\nOn the public face a refusal counts in the peer's bucket. A running mesh needs nothing, and a\nrequest that succeeds is never throttled.\n\n### What stops working\n\n`ManagerServiceAuthorityCtx` from `@cotal-ai/auth` now requires `failures` and `badCaps`, so a host\nthat builds it in an object literal without them fails to compile. The handler now calls the\n`peerKey`, `throttled` and `recordFailure` of the policy it is given. After 30 refusals in a minute\non one face, a further refusal on either route answers 429 without its reason.\n\n### Before the upgrade\n\nPass the `failures` and `badCaps` arrays your `/exchange` route counts in, so both routes share one\nbudget per face. A host that serves only this route can pass new empty arrays.\n\n## Primitive scope kind in 0.78.0\n\n`PrimitiveSpec` from `@cotal-ai/lang` replaces `opensScope: boolean` with `scope: ScopeKind | null`,\nthe scope kind a primitive opens, in the way `kind` names the effect it journals. Both engines now\nread that kind from the table. Before, they cast the primitive's name to `ScopeKind`, so a scope row\nmissing from the union compiled and reached the journal as a kind its readers did not know. A\nrunning mesh and its journals need nothing.\n\n### What stops working\n\nCode that reads `opensScope` from `PRIMITIVES` or a `PrimitiveSpec` fails to compile, and plain\nJavaScript reads `undefined` there, so a check on it never holds.\n\n### Before the upgrade\n\nRead `spec.scope !== null` where you read `spec.opensScope`, or `spec.scope` when you need the kind.\n\n## Token verifier clock tolerance in 0.77.0\n\n`validateUserToken`, and the `IdpConfig` that `verifyIdpToken` and `createIdpBridge` take, from\n`@cotal-ai/auth` no longer accept `clockToleranceSec`. Both verifiers allow a fixed 5 seconds of\nclock skew on `exp`, `nbf` and `iat`, the default they already applied. Nothing in the repository\npassed the option, so a running mesh behaves as before. Only code of your own that passes it is\naffected.\n\n### What stops working\n\nA call or `IdpConfig` that passes `clockToleranceSec` in an object literal no longer compiles. Plain\nJavaScript that keeps it still runs, and the value is ignored. A `NaN` value, such as `Number()` of\nan unset environment variable, used to turn off the expiry, not-before and issued-at checks in both\nverifiers, so an expired or post-dated token was accepted. A large value widened those checks by\nthat many seconds. Both kinds of token are now refused.\n\n### Before the upgrade\n\nRemove `clockToleranceSec` from each call and each `IdpConfig`. A host whose clock is more than 5\nseconds off the issuer's needs its time synchronized.\n\n## Auth context closure in 0.71.0 (unreleased)\n\nExisting deployments need no credential migration or restart for these additive APIs. Embedded\nhosts can now inspect `handle.connections()` and await `handle.closed` after `close()` or `drain()`\nto prove every owned transport ended, including the callout, replaced readiness readers and\nshort-lived clients. The inventory is a detached snapshot.\n\nA transport close failure now rejects with its connection label. The terminal signal stays pending\nwhile any connection remains live. Repair the failure and retry `close()` before awaiting\n`handle.closed`. Closing one hosted context does not close another account's context.\n\nRead a space's claim with `readPlaneClaim(kv, space)` on that account's leader-only auth bucket.\nAn unclaimed space returns `undefined`; held and released rows retain their claim identity.\nDeleted, malformed and foreign-space rows refuse. `PlaneClaimRow` and `PLANE_CLAIM_KEY` are exported.\n\nUse `observeAccountLivenessWithCreds({ servers, observerCreds, accountId, options })` with the\naccount-scoped membership-observer credential to list that account's connections. It never widens\ncredentials or evicts connections. Zero rows prove absence only with a complete sweep and the\nsingle-server proof. An embedded endpoint's trusted composition can retain transport custody\nthrough `EndpointOptions.onConnection`.\n\n## Unreleased\n\nOn a per-user-auth mesh, a spawn-scoped caller can arm the event plane of a child under its own\nowner without `admin`. This fixes owned spawns in spaces whose registration policy requires the\nplane. Upgrade the manager to pick up the admission change. No credential or state migration is\nneeded. Cross-owner arming still requires `admin`, and the child's own-channel rule and ledger\nenvelope are unchanged. A silent non-owner caller in a space without the policy still has the\nplane disarmed, with a notice if provisioning succeeds.\n\n## Hosted auth plane identity in the store (unreleased)\n\nA hosted context started through `startAuthService` keeps its auth plane instance identity in the\ninjected `SecretStore` under `authInstanceKey(space)`, with the other auth secret kinds. It used to\nsit under `stateDir` at `.cotal/space.<hex>/auth-instance.json`, though `stateDir` holds non-secret\nstate and the record holds the plane's private serve seed. The first start of an upgraded context\nputs that record into the store, removes the file and keeps the instance. A CLI root is unchanged.\n\n### What stops working\n\nA start refuses when the store and `stateDir` hold different instance identities, and names both. A\nstore that refuses a put of the new key fails the start.\n\n### Before the upgrade\n\nLet the store accept a put of `authInstanceKey(space)`. A copy of `stateDir` taken before the upgrade\nstill holds the serve seed, so delete it or protect it as secret material.\n\n## Injected SecretStore naming in 0.76.0\n\nThe manager and the delivery daemon name an injected `SecretStore` through\n`injectedSecretStoreIdentity` from `@cotal-ai/core`. The rule is unchanged: the store's declared\nidentity, else the coordinate in `COTAL_SECRET_STORE`, else a refusal. A running mesh needs nothing.\n\n### What stops working\n\n`reloadStoreIdentityOf` from `@cotal-ai/delivery` takes `{ injected: true, store }` or\n`{ injected: false, identity }`. A call that passes `injected: true` with no store fails to compile,\nand plain JavaScript gets a `TypeError`. Both processes refuse an unnamed injected store with one\nmessage, which starts `an injected SecretStore must declare its identity`, so a log match on either\nold message no longer matches.\n\n### Before the upgrade\n\nPass the injected store to `reloadStoreIdentityOf`, or call `injectedSecretStoreIdentity(store)`.\n\n## Manager-service authority policy flag in 0.75.0\n\n`handleManagerServiceAuthority` from `@cotal-ai/auth` no longer reads `allowManagerAuthority` from\nits policy argument, and the field is removed. Both exchange faces set it to `true`, so it never\nrefused anything. A running mesh needs nothing: the public listener serves\n`POST /manager-service-authority` as it did before, and the docs now list that route.\n\n### What stops working\n\nA host that serves the route itself and passes a policy object literal with\n`allowManagerAuthority` fails to compile with TS2353. Plain JavaScript that set it to `false` got a\n403 from the handler, and its requests now reach the capability and IdP checks.\n\n### Before the upgrade\n\nRemove `allowManagerAuthority` from the policy you pass. Where you set it to `false`, leave the\nroute out of that listener's route table instead.\n\n## Hermes model from the environment in 0.68.0\n\nA connector now launches on the model and variant its launcher resolved (the `--model` or\n`--variant` flag, else the agent file's `model:` or `variant:`) and no longer reads them again from\nthe agent file. The Hermes connector also no longer takes a model from `HERMES_MODEL` in the\nenvironment of the process that spawns the seat, including when `spawn.env` lists it.\n\n### What stops working\n\nA Hermes spawn whose only model was `HERMES_MODEL` in the spawning environment is refused at launch,\nand the refusal names both ways to set a model. Spawns that set `--model` or `model:` are unchanged,\non every connector.\n\nCode that calls a connector's `buildLaunch` directly with only `configPath` now gets no model or\nvariant from that file. Pass them as `model` and `variant`.\n\n### Before the upgrade\n\nMove each Hermes seat's model from `HERMES_MODEL` onto its spawn with `--model`, or into its\npersona's `model:`.\n\n## Run answers on a participant manager in 0.68.0\n\nA participant manager now asks its issuing host for an answering credential by naming the run and\nstep it answers. The host reads the pause's token off that run's journal and no longer accepts a\ntoken from the manager. Runs on a mesh with no participant manager are unaffected.\n\n### What stops working\n\nWhile a participant manager and its issuing host run different sides of this release, the host\nrefuses every `cotal run answer` and every amendment that manager serves, because each side refuses\nthe other's request shape. Starting, resuming and reading runs is unchanged. A pause stays waiting\nthrough the window, or follows its timeout if it has one.\n\n### Before the upgrade\n\nUpgrade the auth service and every participant manager registered with it in the same window, then\nanswer the pauses that waited.\n\n## Headless OpenCode handshake in 0.69.0\n\nWith `COTAL_SERVE_HEADLESS=1`, the OpenCode launcher's `[cotal-serve]` line on stdout now carries\nonly `port` and `session`. The server password no longer appears in it, and the 1.x TUI no longer\nreceives the password on its command line.\n\n### What stops working\n\nA headless host that read `password` from that line has no password, and the server refuses its\nrequests. Seats with a TUI, and headless seats that no host drives, are unaffected.\n\n### Before the upgrade\n\nHave each headless host mint a password and pass it to the launcher as `OPENCODE_SERVER_PASSWORD`,\nthen use it for basic auth as before. Without that variable the launcher mints its own.\n\n## Filesystem store identity in 0.69.0\n\nThe delivery daemon's answer to the manager's store check now names a filesystem store by its root\nand by a random `id` that the store records once in `store.id` inside its own directory:\n`.cotal/store.id` for a workspace root, or the directory of the file for `cotal deliver --creds\n<file>`. A manager no longer counts the daemon's store as its own because the two roots have the\nsame path. On a split whose broker host and manager host use one root path, the manager host now\nstays off the daemon-credential renewal lease, so `cotal doctor auth --fix` on the broker host can\nrenew the daemon credentials.\n\n### What stops working\n\nA manager and a delivery daemon on different sides of this release refuse each other's answer to\nthe store check. The manager then remints no daemon credential, and a manager that is booting does\nnot start. This is read from the code and was not measured across two releases. A\n`cotal deliver --creds <file>` whose directory is a read-only mount and holds no `store.id` stops at\nstart. So does a `--creds` file that is its directory's `store.id` under any name, and a `store.id`\nthat is a symbolic link or holds anything but a lowercase UUID.\n\n### Before the upgrade\n\nUpgrade the broker host and every manager host of a space in the same window. For a `--creds` file\non a read-only mount, add a regular `store.id` file beside it that holds a new lowercase UUID and no newline,\nas `node -e 'process.stdout.write(crypto.randomUUID())' > store.id` writes. Move a `--creds` file\nnamed or linked as `store.id` to a file of its own.\n\n## Detached spawns with `--share-tools` in 0.69.0\n\nThe manager's `spawn` operation now takes `shareTools` as a list of MCP server names. The CLI parses\n`--share-tools` into that list before it sends the request, and the manager cluster document moves\nto revision 22. A cut taken with `cotal down --preserve-state` before the upgrade still resumes: the\nmanager reads its `cotal-manager-resume/v1` inventory and writes new cuts as\n`cotal-manager-resume/v2`.\n\n### What stops working\n\nA CLI and a manager on different sides of this release refuse a detached spawn that passes\n`--share-tools`, because the CLI checks each request against the contract the manager serves. This\nis read from the code and was not measured across two releases. A detached spawn without the flag,\na foreground spawn and a roster entry are unaffected. A manager older than this release cannot\nresume a cut that this release took.\n\n### Before the upgrade\n\nUpgrade the CLI on every host that runs `cotal spawn --detach` in the same window as the managers\nit reaches.\n\n## Shared MCP server checks in 0.69.0\n\nThe cotal config reader now checks each server under `connectors.<name>.mcpServers` when it reads\nthe file, and refuses one that cannot launch as written, naming the file and the field. The rules\nare in [the config file](config.md#the-config-file).\n\n### What stops working\n\nA config file that holds such a server refuses every Claude spawn that reads it, including one with\n`--share-tools none`. Before, a field of the wrong type failed each Claude spawn that shared the\nserver with a `TypeError` that named neither the file nor the server, a spawn that did not share it\nlaunched, and a server with no `command` or `url` was passed to `claude`, which never started it.\nRead from the code and not measured: spawns on other connectors, a manager resume and the step of\n`cotal setup` that records the shared list read the same files, so each stops at the same refusal.\n\n### Before the upgrade\n\nCheck `connectors.<name>.mcpServers` in the operator-level config file and in each space's\n`.cotal/config.json`. Give each server a string `command`, or a `type` of `http`, `sse` or `ws` with\na string `url`. Write `args` as a list of strings and `env` and `headers` as objects of strings, or\nremove the server.\n\n## Remote manager family eviction in 0.69.0\n\nA remote manager registered through its host now asks the host to evict up to 256 holders of its\ncredential family in one maintenance request, and the host reads the family once for the whole set.\nBefore, a restart sent one request per holder and the host read the whole family for each one.\nMeshes with no remote manager are unaffected.\n\n### What stops working\n\nWhile a remote manager and its issuing host run different sides of this release, each side refuses\nthe other's eviction request shape. A restart whose credential family already has holders then fails\nat its eviction step and leaves the manager's registration gate frozen. A first start, a clean stop\nand the host's reconciliation of a foreign slot holder are unchanged.\n\n### Before the upgrade\n\nUpgrade the auth service and every remote manager registered with it in the same window. A manager\nthat restarted inside the window resumes its frozen registration on its next start once both sides\nrun this release.\n\n## AG-UI emitter holder hooks in 0.69.0\n\n`AguiEmitterHolder` from `@cotal-ai/connector-core` now takes its hooks as one named object after\nthe emitter factory: `new AguiEmitterHolder(startEmitter, { onError, onRunClosed, waitLive, runMeta })`.\nOnly `onError` is required. Nothing about a running mesh changes, and every shipped connector passes\nits hooks by name. Only a connector of your own that builds a holder is affected.\n\n### What stops working\n\nA holder built with positional hooks, such as `new AguiEmitterHolder(start, onError, onRunClosed)`,\nno longer compiles, because the constructor takes two arguments. Plain JavaScript that keeps the\npositional form still runs, but the holder calls none of its hooks, so a failure never reaches\n`onError`.\n\n### Before the upgrade\n\nPass each hook by name, for example `new AguiEmitterHolder(start, { onError, onRunClosed })`, and\ndrop any `undefined` that filled an earlier slot to reach a later hook.\n\n## Worker run failure type in 0.70.0\n\n`WorkerRunFailed`, the failed result of `runInWorker` in `@cotal-ai/lang`, is now a union on\n`class`: `released`, `held`, `effect`, `too-large`, `rejected` or `error`. A running mesh needs\nnothing, because the runtime host and the engine thread ship in the same install. A run on the\ncompiled engine whose program throws an object with `code: \"L5012\"` or `code: \"L5025\"` used to end\nreleased and now ends failed, as it does on the walker.\n\n### What stops working\n\nTypeScript code that reads `code`, `reason`, `step`, `pending`, `kind`, `detail` or `tooLarge` on a\n`WorkerRunFailed` it has not narrowed fails with TS2339. A released, held, too-large or rejected\nresult no longer carries `code`, so JavaScript that branched on `L5012`, `L5025`, `L5006` or\n`L5010` stops matching with no error. `tooLarge` is gone.\n\n### Before the upgrade\n\nBranch on `class` where such code read `code`: `released` for L5012, `held` for L5025, `too-large`\nfor L5006 and `rejected` for L5010. An `effect` or `error` result keeps its `code`. Once narrowed to\n`too-large`, a result carries the `stepKey`, `bytes` and `bound` that `tooLarge` held.\n\n## Remote manager request builder in 0.70.0\n\n`remoteManagerClient.remoteManagerAuthorityRequest` from `@cotal-ai/manager` now takes an\noperation's coordinates as one object, and `remoteManagerRegistrationProof` from `@cotal-ai/core`\ncomputes the proof from the manager's identity state instead of a request. Nothing about a running\nmesh changes: the proof digest and the request on the wire are the same, so a manager and a host on\ndifferent sides of this release still accept each other. Only code that builds remote manager\nrequests itself is affected, in TypeScript and in plain JavaScript.\n\n### What stops working\n\nA call that passes the registration proof, contract artifacts, session, retirement or transfer\nreader as positional arguments after the operation no longer compiles. A call that passes a request\nto `remoteManagerRegistrationProof` no longer compiles either, because the second argument now names\nthe lifecycle `lifecycleUid`, as the identity state does.\n\nPlain JavaScript runs both old calls without an error. The builder drops the positional coordinates,\nso the host refuses the request with `requires a sha256 registrationProof`. A proof computed from a\nrequest leaves out the lifecycle, so the host refuses a request that carries it as a proof mismatch.\n\n### Before the upgrade\n\nName the coordinates, for example\n`remoteManagerAuthorityRequest(state, \"cli\", \"retire\", { registrationProof, retirement })`.\nCompute the proof as `remoteManagerRegistrationProof(owner, state)`, adding the contract artifacts\nas a third argument for activation only. A host that recomputes the proof from a received request\npasses `{ space, instanceId, lifecycleUid: managerLifecycleUid, identities }` from that request.\n\n## Bearer validator lifetime cap in 0.70.0\n\n`validateUserToken` from `@cotal-ai/auth` no longer takes `maxTtlSec`. It caps a bearer's lifetime\nat the cap of the bearer's view, the same cap the issuer applies when it mints: 900 seconds, or 300\nfor a `transfer-writer` bearer. The auth callout never passed the option, so a running mesh behaves\nas before. Only code of your own that calls the validator with `maxTtlSec` is affected.\n\n### What stops working\n\nA call that passes `maxTtlSec` in an object literal no longer compiles. Plain JavaScript that keeps\nit still runs, and the value is ignored. A `NaN` value, such as `Number()` of an unset environment\nvariable, used to turn the lifetime check off and accept a bearer of any lifetime. That bearer is\nnow refused at its view's cap.\n\n### Before the upgrade\n\nRemove `maxTtlSec` from each call. A test that needs a bearer to expire sooner mints one with a\nshorter lifetime.\n\n## Persisted identity records in 0.70.0\n\nThe manager instance identity, the manager sibling identities, the auth plane instance identity and\na participant manager's remote authority state now share one reader and one first mint in\n`@cotal-ai/workspace`, exported as `claimIdentityRecord` with the nkey check `identityOf`. Each\nrecord is read as a regular file, must hold non-empty nkeys and is created exclusively, so\nconcurrent first starts of a participant manager on one root now settle on one identity where each\nused to keep its own. `saveManagerInstanceIdentity` and `saveAuthInstanceIdentity` are gone. A\nrunning mesh whose records are plain files needs nothing.\n\n### What stops working\n\nA manager instance, auth instance or remote authority record that is a symlink, a directory or any\nother non-regular entry is refused where it used to be followed. The manager, the auth plane and a\nparticipant manager fail to start on it, and `cotal reconcile-gate` and `cotal deregister-instance`\nrefuse it. Retirement already refused it. A remote authority record with an empty nkey id or seed\nis refused too. A first mint that loses its race and cannot read the winner now refuses with\n`identity-record-create-lost` in place of `manager-instance-identity-create-lost` or\n`auth-instance-identity-create-lost`. Code that imports either `save` function no longer compiles.\n\n### Before the upgrade\n\nReplace a symlinked identity record with a copy of the file it points to. Code that wrote a record\nwith a `save` function plants it with `createManagerInstanceIdentity` or\n`createAuthInstanceIdentity`, which create the record when it is absent and otherwise return the\nstored one unchanged. Nothing replaces an overwrite of a stored identity.\n\n## Manager instance in user credentials in 0.70.0\n\n`AuthProvider.userCredentials` from `@cotal-ai/core` no longer returns `managerInstanceId`. A\n`manager-caller` credential's manager instance is the signed `act.managerInstanceId` claim in its\nbearer, which the broker verifies and the CLI already used. The reference provider in\n`@cotal-ai/auth` stops copying the exchange response's field into its result, where nothing\ncompared it with the bearer. The exchange still answers with the field, so a running mesh behaves as\nbefore.\n\n### What stops working\n\nCode of your own that reads `managerInstanceId` from a `userCredentials` result no longer compiles,\nand plain JavaScript reads `undefined` there.\n\n### Before the upgrade\n\nRead the instance from the bearer's `act.managerInstanceId` claim.\n\n## Auth plane identity location in 0.70.0\n\nThe user-auth service keeps its instance identity in the root's `.cotal/space.<hex>/auth-instance.json`,\nbeside the manager's. It used to sit inside `.cotal/auth`, at\n`space.<hex>/.cotal/auth/auth-instance.<hex>.json`, so a copy of that folder carried it. The first\nstart of an upgraded root moves the record and keeps the instance. A hosted context started through\n`startAuthService` has its record moved the same way inside its `stateDir`.\n\n### What stops working\n\nCode that calls `openAuthAuthorityPlane` without the new `identityRoot` option no longer compiles. A\nstart that finds a record both in `.cotal/space.<hex>/` and at its older place refuses and names the\ntwo files. A start also refuses when the older place of the auth or manager identity holds a symlink,\na directory or anything else that is not a regular file. The manager used to skip a dangling symlink\nthere and mint a new identity.\n\n### Before the upgrade\n\nPass `identityRoot` to `openAuthAuthorityPlane`. When `dir` is a workspace root's user-auth state\ndir, `<root>/.cotal/auth/space.<hex>`, pass that root. A plane with no workspace root, as\n`startAuthService` runs, passes `dir` itself. Either keeps the identity the plane already has: on the\nfirst start it moves from `<dir>/.cotal/auth/` to `<identityRoot>/.cotal/space.<hex>/`. Never pass a\ndirectory inside `.cotal/auth`: the record would land in the folder an operator copies and travel\nwith it again.\n\nA copy of `.cotal/auth` taken from a root last run by an older Cotal carries that root's record.\nDelete `.cotal/auth/space.<hex>/.cotal/auth/auth-instance.<hex>.json` from the root you copied it to\nbefore the first `cotal up --user-auth` there.\n\n## Per-seat `COTAL_` names in `spawn.env` in 0.71.0\n\n`spawn.env` in the cotal config no longer forwards a `COTAL_` name the launcher sets for each seat,\nsuch as `COTAL_ROLE`, `COTAL_MODEL` or `COTAL_SUBSCRIBE`. Before, a seat launched with no value of\nits own took the spawning process's value and ran under that role, model or read set. The\nmachine-wide knobs a seat already receives, such as `COTAL_HOME`, may still be listed.\n\n### What stops working\n\nEvery spawn and resume under a config whose `spawn.env` lists such a name is refused before\nlaunch, and the refusal names the entry. Code that calls `launchEnv` from `@cotal-ai/connector-core`\nwith such a name in `envAllow` gets the same error.\n\n### Before the upgrade\n\nRemove those names from `spawn.env`. Give each seat its role, model and channels with `--role`,\n`--model` and `--subscribe`, or in its persona's `role:`, `model:` and `subscribe:`.\n\n## Role addresses in 0.71.0\n\nA role must be one `[A-Za-z0-9_-]` token. Before 0.71.0 any other spelling was rewritten into one:\n` probe ` reached the `probe` queue and `pro.be` reached `pro_be`, while the message kept the\nspelling sent. An anycast to `*` was accepted and stored where no holder reads it.\n\n### What stops working\n\nAn agent whose role is outside the token set no longer starts, however it is launched:\n`cotal join --role`, `cotal spawn --role`, an agent file's `role:`, `COTAL_ROLE` and an embedded\nendpoint's `card.role` are all refused before the agent joins.\n\nA send to such a role, or to `*`, through `cotal send ask`, `/anycast` or `cotal_anycast` is refused,\nand nothing is stored.\n\n`routeToken` is no longer exported from `@cotal-ai/core`. A role routes as spelled, so code that\nused it to name a role's queue uses the role itself, and `assertValidRole` checks one.\n\n### What migrates on its own\n\nEvery task queue. A `svc_<role>` durable was always named from the rewritten token, so its pending\nrequests and its holders carry over.\n\n### Before the upgrade\n\nRename each role outside the token set to the token it already routed to: remove the surrounding\nspaces and replace every other character outside the set with `_`. Rename it where the holder is\nlaunched and in every script or prompt that sends to it.\n\n## IdP URLs on `localhost` in 0.73.0\n\nThe IdP URL that `cotal login --idp` and `cotal up --user-auth --idp` take, the JWKS URL the auth\nservice pins from it, the space-catalog link and `cotal sync` now share one rule: `https://`, or\n`http://` on a loopback IP literal. `localhost` is a name, so it no longer counts: a hosts entry\nwould choose the IdP, and with it the keys the callout trusts. Every loopback literal now passes, so\n`http://127.0.0.2/api/auth` and `http://[::ffff:127.0.0.1]/api/auth` are accepted where they were\nrefused before. A JWKS URL with any scheme other than `https:` or `http:` is refused. A mesh whose\nIdP uses HTTPS is unaffected.\n\n### What stops working\n\nOn a mesh whose IdP is pinned at `http://localhost:<port>/...`, the auth service builds its key\nresolver from the pinned JWKS URL at start, and that resolver now refuses it with\n`JWKS origin must be https (or http on a loopback IP literal for dev)`.\n\n`cotal login` and `cotal logout` with `--idp http://localhost:<port>/...` are refused with\n`idp url must be https (or http on a loopback IP literal such as 127.0.0.1 for local dev)`, and so is\nevery command that reads a session cached under that URL.\n\n### Before the upgrade\n\nUse `127.0.0.1` (or `::1`) in place of `localhost`. In the space's `idp.json` under the mesh's\n`.cotal/auth`, change `url` and `jwksUri` to the literal spelling and leave `issuer` and `audience`\nas they are. Owners derive from the issuer, so existing grants keep matching. Change a manifest's\n`broker.idp` the same way, because `cotal up` refuses an `--idp` that differs from the pin. Then\nhave each person run `cotal login --idp http://127.0.0.1:<port>/api/auth` again, because sessions\nare cached under the URL.\n\n## Terminal layout `Pane.confirm` in 0.73.0\n\n`Pane.confirm` is removed from `@cotal-ai/core`, and the tmux and cmux terminal-layout providers no\nlonger press Enter in the panes they open. The flag carried no prompt text, so both providers\npressed Enter five times, one second apart: the first dialog a pane showed was answered with its\ndefault, and a prompt that never appeared went unreported. Nothing in Cotal sets it, so a running\nmesh needs nothing. Spawned agents are unaffected, because their runtimes match the startup prompt\nby its text.\n\n### What stops working\n\nA `Pane` object literal that sets `confirm` fails to compile with TS2353. Plain JavaScript that sets\nit runs without an error, and the pane stays at its prompt.\n\n### Before the upgrade\n\nRemove `confirm` from each `Pane`. Start a command that shows a startup prompt with the option that\nskips it, or answer the prompt in the pane.\n\n## Carrying a resumed Claude session to another host in 0.67.0\n\n`cotal spawn --resume <id> --detach --on <instance>` now carries a Claude session held on the\noperator's host to the target manager instance. Both sides need this release: an older manager does\nnot serve `transcript-receive`, and the CLI then stops with that manager's refusal instead of\nlaunching. The manager cluster document moves to revision 21, and the `ps` row's `resume` object\ngains `host` and `transferredAt`.\n\nA manager host that runs carried seats needs `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_AUTH_TOKEN` or a\ncloud provider selection in its environment, because each carried seat runs in its own Claude home\nwith no stored login. On an authenticated mesh the CLI mints the transfer writer from the space's\nsigning seed, so the carrying host needs that seed, as for any other operator command. On a user-auth\nmesh it exchanges the operator's login for a `transfer-writer` view instead, so the operator's grant\nneeds scope `admin`, and the auth service must run this release. A remote manager receives a carry once\nits host serves the manager-service `transferReader` operation. A seat launched without carrying,\nincluding any `--resume` whose id this host does not hold, is unchanged.\n\n## Lifecycle head type in 0.67.0\n\n`LifecycleMapping`, the type `parseLifecycleHead` returns, is now a union on `state`. Nothing about\na running mesh changes: heads that parsed before parse the same way, and the refusals are\nunchanged. Only TypeScript code that compiles against `@cotal-ai/core` is affected.\n\n### What stops working\n\nAn `interface` that extends `LifecycleMapping` fails with TS2312, because an interface cannot extend\na union. Code that builds a head in memory no longer compiles when the head is `retiring` without\nits `op`, or `active` or `retired` with one. The parser already refused those heads.\n\n### Before the upgrade\n\nDeclare such an interface as an intersection instead, for example\n`type ActiveMapping = LifecycleMapping & { state: \"active\" }`. A reader that has checked\n`state === \"retiring\"` reads `op` without a guard.\n\n## Issuance gate types in 0.67.0\n\n`EpGateRow` and `EndpointGateRow`, which `parseIssuanceGate` and `parseEndpointGate` return, and\n`EpGateState`, which an `EpIssuanceGate` or `EpIssuanceBarrier` returns from `observe`, are now\nunions on `state`. Nothing about a running mesh changes: gates that parsed before parse the same\nway, and the refusals are unchanged. Only TypeScript code that compiles against `@cotal-ai/core`\nis affected.\n\n### What stops working\n\nAn `interface` that extends one of these types fails with TS2312, because an interface cannot\nextend a union. Code that builds a gate in memory, such as a custom barrier's `observe`, no longer\ncompiles when the gate is `frozen` or `retired` without its `op`, or `open` with one. The gate\nparsers already refused those rows.\n\n### Before the upgrade\n\nDeclare such an interface as an intersection instead, for example\n`type CustomGateRow = EpGateRow & { custom: string }`.\n\n## Lifecycle-blocked refusals in 0.66.0\n\nA refusal that carries `ai.cotal.ep.lifecycle-blocked` now reports only the lifecycle state it\nread. Nothing about a running mesh changes. A client that branches on the detail must read the new\nfield.\n\n### What stops working\n\nA refusal raised at the issuance gate used to carry `headState` without reading the head:\n`retiring` for a frozen gate and `retired` for a retired one. It now carries `gateState`\n(`frozen` or `retired`) and no `headState`. A client that treats `headState: \"retired\"` as a\nburned uid, or `headState: \"retiring\"` as a retirement in flight, no longer matches those\nrefusals, and the `[lifecycle ...]` suffix on the error string changes the same way. A custom\nissuance barrier whose `observe` returns a frozen gate without a valid `op` (a string `opId` and\none of the four op kinds) is now refused as `internal` by `registerServiceInstance`.\n\n### Before the upgrade\n\nUpdate such a client to read `gateState` for a gate refusal and `blockedOp` for the operation that\nholds the gate. `headState` is present only when the refusal read the head, for example an\nactivation refused because the head is still retiring.\n\n## Workflow programs that bind `once` in 0.65.0\n\n`once` is now a scope of the workflow language, so it is a reserved name. A program that declares\nits own `once` binding (`const once = ...`, a parameter or a function named `once`) is refused at\nvalidation with L2002. Nothing else about a running mesh changes.\n\n### What stops working\n\nA run whose recorded program binds `once` cannot be resumed after the upgrade, because a resume\nvalidates the recorded program again. A new `cotal run start` of such a program is refused before\nanything is recorded.\n\n### Before the upgrade\n\nList the runs with `cotal run ps` and check each program that is still running or held for a\nbinding named `once`. Let those runs finish on the old version before you upgrade the manager, and\nrename the binding in the program before you start it again.\n\n## From 0.58.0 to 0.59.0\n\nEvery connector now publishes a failed run's `RUN_ERROR` on `events.<owner>.<actor>` with the fixed\nmessage `run failed` and no `code` or `rawEvent`. The error text and error kind a harness reports\ncan echo a prompt, a peer message or tool output, and that channel has a different read ACL. A\nreader that showed the message or branched on `code` gets neither after the upgrade. Where a\nconnector reports the error kind as the agent's presence condition, that is unchanged.\n\n### Settle pending event frames before the upgrade\n\nEach session's events are frozen in its event write-ahead log before they are published. A session\nrestarted on 0.59.0 whose log still holds an unacknowledged frame with an older `RUN_ERROR` does not\nrepublish it: its event emitter halts with `egress-run-error` and publishes nothing further for that\nsession. The broker may or may not already hold that frame, so the halt cannot settle it.\n\n1. Stop the seats cleanly on 0.58.0, with the broker still up.\n2. List the logs that still hold a pending frame. The logs live under the events state root\n (`COTAL_WORKSPACE_ROOT`). Empty output means there is nothing to settle.\n\n ```sh\n find \"$COTAL_WORKSPACE_ROOT/.cotal/events\" -name wal.json \\\n -exec jq -r 'select(.pending != null) | input_filename' {} +\n ```\n\n3. For each session listed, start it again on 0.58.0 while the broker is reachable, let it recover,\n stop it, and run step 2 again. Recovery publishes the frame as 0.58.0 would have, error text\n included, so it only finishes what 0.58.0 had already started.\n\n If that start halts with `cas-loss` instead, the agent's subject is no longer at the sequence this\n log expects, and no restart settles that log, on 0.58.0 or later. A lost acknowledgement is one\n cause: the broker stored the frame, so it and its error text are already on the channel, and every\n retry halts the same way because the stream checks the frozen expectation before it deduplicates.\n The halt message names the other causes, such as a second emitter for the same agent under a\n different state root, a restored stream or frontier record, or a purged channel. With those the\n pending frame may never have reached the broker, so a `cas-loss` does not tell you whether it\n landed. Find and stop any second writer and rule out a restored state first. Clearing the halt\n then means purging the agent's event channel and removing the agent's directory under the events\n state root whole (see [Event plane](connect-claude.md#event-plane)). That abandons the pending\n frame whether or not the broker has it, and the purge also drops the earlier frames of every\n session of that agent.\n4. Upgrade once step 2 prints nothing.\n\nIf a session halts with `egress-run-error` after the upgrade, go back to step 3 for that session on\n0.58.0. Do not edit or delete `wal.json` on its own to get past either halt: clearing the pending\nframe abandons that epoch, an event the broker never received is lost, and removing part of the\ndirectory leaves a state the next start refuses.\n\n## Explicit actor grants in 0.59.0\n\n`cotal actor grant` no longer fills an omitted ACL flag with its wide default. A grant names\n`--scope`, `--allow-subscribe` and `--allow-publish`, or passes `--full` to give the ones it leaves\noff their wide defaults (`spawn,role:default`, `>` read, `>` post). Any other grant is refused. The\nbreak is in the CLI on the machine that holds the actor ledger, the one that ran\n`cotal up --user-auth --idp <url>`. No stored row, credential or wire message changes.\n\n### What keeps working\n\nExisting actor ledger rows keep the authority they were granted, and their users and agents connect\nas before. `actor revoke`, `actor list` and a `grant` that names all three ACL flags behave as they\ndid on 0.58.0. Nothing on disk is converted.\n\n### What stops working\n\nA grant that leaves off any of the three flags without `--full` exits 1 with\n`refusing to grant \"<actor>\" with --scope, --allow-subscribe, --allow-publish left off`, naming the\nflags it is missing, and then prints both accepted forms. It writes no row and does not retire the\nactor's current lifecycle. An existing row stays as it was, and an actor granted for the first time\nstays out until the grant is run again. This includes the bare grant printed on 0.58.0 by\n`cotal login`, `cotal status`, `actor list` and the not-granted refusal. Look for it in provisioning\nscripts, onboarding runbooks and anything that pastes those hints.\n\n### Upgrade order\n\nChange the scripts before the ledger machine is upgraded, and make each grant name all three flags.\n0.58.0 and 0.59.0 both accept that form. To keep a wide row, write its defaults out:\n\n```sh\ncotal actor grant <actor> --sub <IdP subject> \\\n --scope spawn,role:default --allow-subscribe '>' --allow-publish '>'\n```\n\nSwitch to `--full` only once the ledger machine runs 0.59.0. 0.58.0 refuses it with\n`Unknown option '--full'` before it reads the ledger. Brokers, managers and participant machines\nneed nothing for this break, so their order is the one the section above gives.\n\n### The window\n\nThis break has no outage. No process restarts for it, and a refused grant changes nothing. The\nexposure is a grant script that runs against 0.59.0 before it was changed: it fails and grants\nnothing.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. On the ledger machine, save the output\nof `cotal actor list` to compare rows after the changed scripts run, and list the scripts that call\n`cotal actor grant`.\n\n### The upgrade end to end\n\n```sh\n# on the ledger machine, still on 0.58.0\ncotal actor list > actors-before.txt\ngrep -rn 'actor grant' <your provisioning scripts>\n# make every grant name --scope, --allow-subscribe and --allow-publish, run them, then upgrade\nnpm i -g cotal-ai@0.59.0\ncotal actor list | diff actors-before.txt -\n```\n\nBoth refusals quoted here were run on 0.58.0 and on the 0.59.0 code. That brokers, managers and stored\nrows need nothing is read from the change, which touches only the CLI and its hints, and was not run\non a live split deployment.\n\n## Repeated flags refused in 0.59.0\n\nA `cotal` flag given more than once is now a usage error unless the command declares it repeatable.\nOn 0.58.0 the last value won with no message, so `cotal down web --space a --space b` acted on `b`\nwhile a wrapper that checked the first `--space` verified `a`. The break is in the command-line\nparser on the machine that runs the command, including commands added with `cotal ext add`. No\nstored state, credential or wire message changes.\n\n### What keeps working\n\nA command line that gives each flag once parses as it did on 0.58.0, in any order and in the\n`--flag=value` form. Flags whose help says repeatable, such as `--opt` and `down --session-store`,\nstill collect every value. A flag-shaped word after `--` is still a positional. The daemons, units\nand agents that `cotal` starts for itself are given each flag once, so a fleet driven only by `cotal`\ncommands typed by hand needs no action.\n\n### What stops working\n\nA command line that repeats any other flag exits 1 before the command runs. It prints\n`Option '--space' cannot be repeated`, or `Option '-f, --file' cannot be repeated` for a flag with a\nshort form, followed by the command's help. `-f` and `--file` count as the same flag. Look for it in\nscripts, aliases and wrappers that append a flag to override one set earlier, such as a fixed\n`--space` followed by `\"$@\"`.\n\n### Upgrade order\n\nChange those scripts first so each flag is given once. 0.58.0 and 0.59.0 both accept that form.\nBrokers, managers and participant machines need nothing for this break, and each machine's CLI\napplies it when that machine is upgraded, so their order is the one the sections above give.\n\n### The window\n\nThis break has no outage. No process restarts for it, and a refused command does nothing. The\nexposure is a script that still repeats a flag when it runs on 0.59.0: it exits 1 instead of acting on\nthe last value.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. List the scripts, aliases and wrappers\nthat call `cotal` so each one can be checked.\n\n### The upgrade end to end\n\n```sh\n# still on 0.58.0\ngrep -rn 'cotal ' <your scripts and wrappers>\n# give each non-repeatable flag once, then upgrade\nnpm i -g cotal-ai@0.59.0\n# run each changed script; a repeat left behind exits 1 with the usage error and does nothing\n```\n\nThe refusal and its messages were run against the 0.59.0 parser and `cotal topology view`. That the\nargument lists `cotal` builds for its own processes give each flag once is read from the code, and\nwas not run on a live split deployment.\n\n## Detached spawns from a seat's shell in 0.62.0\n\nOn a static or open mesh, `cotal spawn --detach` run inside a managed seat's shell now launches as\nthat seat. On 0.61.0 it minted a one-shot operator instrument, so the manager recorded that\ninstrument as the spawner and the seat's own `cotal_despawn` of the child was refused with\n`not authorized: <seat> was not spawned by <caller> (admin tier required)`. The break is in the CLI\non the machine where the seats run. No stored state, credential or wire message changes.\n\n### What keeps working\n\n`cotal spawn --detach` from an operator terminal or from a script outside any seat launches as\nbefore, and so does any call with `--creds`, one aimed at a space other than the seat's own, or a raw\nopen target named with `--server` and an unregistered `--space`. A user-auth mesh is unchanged. A seat with\n`capabilities: [spawn]` still spawns from its shell, and can now stop that child with\n`cotal_despawn`. `--on <instance>` from a seat's shell still lands on that manager instance, now as\nthe seat.\n\n### What stops working\n\n- On a static mesh, a seat without `capabilities: [spawn]` can no longer spawn from its shell. Its\n own credential holds no spawn subject, so the broker refuses the request and the command exits 1.\n- A child launched from a seat's shell is now that seat's child, so the manager stops it when the\n seat exits, as it does for a `cotal_spawn` child. A child that has to outlive the seat that\n started it now goes with the seat.\n- A seat launched without `COTAL_SPACE` is placed by its static credential. Every connector sets\n that variable, so this only reaches a hand-built launch: from such a seat's shell, a spawn aimed at\n a static space that holds no credential for the seat is refused instead of running as the operator.\n\n### Upgrade order\n\nOnly the CLI that seats run from their shell changes, which is the one installed on the host where\nthe seats run. Brokers and managers need nothing for this break, so their order is the one the\nsections above give.\n\n### The window\n\nThis break has no outage. No process restarts for it. A child already running when you upgrade\nkeeps the spawner the manager recorded at its launch.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. List the agent files whose seats run\n`cotal spawn --detach` from their shell, note which of them lack `capabilities: [spawn]`, and note\nwhich of their children must outlive the seat.\n\n### The upgrade end to end\n\n```sh\n# still on 0.61.0: find the seats that spawn from their shell\ngrep -rln 'cotal spawn' .cotal/agents\n# add `capabilities: [spawn]` to each of those agent files that lacks it, and launch any child\n# that must outlive its seat from an operator terminal instead\nnpm i -g cotal-ai@0.62.0\n```\n\nThe attribution, the despawn, the refusal of a seat without `spawn`, the stop on seat exit and a\nseat's `--on` spawn were run on a local static mesh, and the attribution and the despawn on a local\nopen mesh.\n\n## From 0.53.0 to 0.54.0\n\nManager calls now borrow an instance-bound `manager-caller` credential. Followed mutations require\n`manager.goal-result` on the selected manager, so a compatible issuer, manager and client must be\nloaded together. An older manager is refused before a followed mutation; upgrading an installed\nbinary alone does not replace code in a running manager, connector or embedded client.\n\n### Preserve state before changing processes\n\nSnapshot the broker's durable storage using its supported backup procedure, the host authority and\nactor ledgers, and each participant's manager identity, runtime custody records, credentials and\nsaved sessions. Include the embedding application's database and configuration under its supported\nbackup procedure. Record the loaded package versions and the CLI path used by bearer helpers.\nKeep these copies private. Do not change the IdP issuer, regenerate manager identities, rotate agent\ncredentials or recreate tenant storage to make the upgrade pass.\n\nNo ledger, goal-history or session conversion is required for this change. Existing ordinary\nmessaging credentials retain their normal expiry rules. New manager-caller credentials are obtained\non demand from the current grant; old manager-call credentials do not gain the new view automatically.\nExisting accepted goals remain durable and must not be submitted again merely because observation\nwas interrupted. Fresh remote registration publishes its service status at the current revision and\nepoch; do not seed that status manually.\n\n### Upgrade the split deployment\n\n1. Stage one pinned 0.54.0 package set for the host and participants, including the embedding SDKs.\n Pause new manager mutations and let accepted work settle where possible before reloading processes.\n2. Upgrade the host issuer and embedding first. Keep the broker, its account identities and durable\n storage in place. Then load the matching manager release on each participating machine.\n3. Preserve active seats through the runtime's supported update path. A Linux custodial runtime may\n release and re-adopt seats within its 600-second unattended window; verify the actual runtime,\n custody records and process identities before relying on it. A legacy PTY runtime without release\n support cannot preserve active seats through a generic manager restart. Drain it at an approved\n idle window instead of signalling the manager or replacing conversations.\n4. Reload the clients and connectors through their session-preserving host controls. Refresh any\n bearer helper captured from an older immutable CLI path. A transport-only reconnect does not reload\n JavaScript. Verify authenticated instance selection, a read-only manager command and canonical\n result recovery before allowing new followed mutations.\n\nTreat the interval from issuer reload through compatible manager/client reload as a manager-control\noutage. Mixed versions can refuse discovery or commands; there is no promised rolling transition.\nOrdinary agent sessions survive only where their runtime and credentials permit it. If verification\nfails, keep mutations paused and repair forward from the preserved state rather than resetting it.\nThis release does not add host-backed enrollment or terminal release for stock participant detached\nagents; see [Remote supervised agents](run-a-mesh.md#remote-supervised-agents).\n\n## From 0.48.2 to 0.49.0\n\n0.49.0 changes how a credential's authority is recorded. A credential is no longer only a signed\nfile: it is an *issuance*, with a generation the issuer chose and durable evidence of the ceiling it\nwas granted under. The important consequence for a running deployment is not at connect time. It is\nat renewal time.\n\n### What keeps working without any action\n\n- **Existing agent credentials keep authenticating.** A credential minted under 0.48.2 is not\n revoked and is not rejected at connect. Nothing needs to be re-issued to bring the fleet back up\n after the upgrade.\n- **The channel registry survives.** Channels, their replay settings, descriptions, and usage text\n are ordinary durable state and are not rewritten by the upgrade.\n- **`cotal deliver` is still a standalone command.** Running the delivery daemon as its own process\n remains supported; it is not restricted to being a child of `cotal up`.\n- **`cotal join` keeps its flags.** In particular `--lifecycle-uid` is not new in 0.49.0. It has\n been required alongside `--creds` since well before this release, and the pairing rule did not\n change here. A scripted external join that worked under 0.48.2 works unchanged.\n\n### What does not migrate\n\n**A credential minted before 0.49.0 cannot be renewed.** Managed agent credentials carry a\n24-hour lifetime and the manager re-signs one once it passes **75%** of its life, ticking every\nquarter of the TTL so a tick always lands inside that window. When the manager reaches a credential\nthat carries no issuance, it refuses to renew it and logs the agent by name:\n\n```\n! managed cred renewal <agent>: renewManagedStaticCred: <agent> carries no issuance;\n a static credential minted before SPEC 13.15 is not renewed under an unbound generation\n - respawn the agent\n - the agent dies loud at this cred's expiry unless it is reminted\n```\n\nSo the fleet comes up fine, runs normally, and then each agent stops at its own credential's\nexpiry, within roughly a day of the upgrade, one at a time rather than together. The refusal is\ndeliberate: the renewal would otherwise have to invent a generation nobody issued, which is the\nstate the release exists to remove.\n\n**Respawn the managed agents as the last step of the upgrade.** For this particular upgrade the\nrespawn is not optional: stopping a 0.48.2 manager ends its agent processes whichever CLI you use,\nfor the reason given under the outage window below. The respawn is how they come back, and it is\nalso what mints each credential as an issuance so it renews from then on. One planned pass over the\nfleet is the whole job. Skipping it leaves agents stopped and, for any credential that survived\ninto 0.49.0 unminted, brings the renewal cliff above a day later, one agent at a time.\n\n### Credentials you minted yourself\n\n**A credential you minted with `cotal mint` is a different case, and it very likely needs\nnothing.** The distinction that matters is not the word \"static\", which covers both. It is **what\nminted the credential and who owns its renewal**. A credential the **manager** minted for an agent\nit spawned carries a lifetime and is renewed by the manager, so it is the subject of everything\nabove. A credential **you** minted with `cotal mint` and handed to an external peer is issued with\n**no expiry at all**, and no manager renews it: it is not in the sweep, so there is no renewal to\nfail. It keeps working after the upgrade, and re-minting it would mean coordinating with a third\nparty for no gain.\n\nThe manager says which one it is holding. Where a credential has no expiry to reach, the sweep\nnames it and moves on rather than refusing:\n\n```\n! managed cred renewal <agent>: credential is unbounded - not renewed\n (a pre-TTL credential stays as minted until respawn)\n```\n\nRe-mint an external peer's credential only if you want it to carry a lifetime, and at a time you\nchoose.\n\n### How to read the boot log\n\nA 0.49.0 manager starting over an existing space may print lines like:\n\n```\n verified evicted: <holder-key> (3/12)\n already verified (durable): <holder-key>\n\u2713 boot self-heal: manager/<id> registration gate reopened at generation <n>\n```\n\nThese are **not** a credential migration, and reading them as one is the most likely way to\nconclude the fleet is fine when it is not. They come from the manager repairing **one** endpoint\nregistration gate that a previous restart left frozen, and they enumerate that single gate's\ncredential-family holders as it verifies each one evicted. `already verified (durable)` on a later\nstart is the repair cursor resuming, not a credential that became durable. The repair is real and\nuseful (it is what previously needed `cotal reconcile-gate` by hand), but it says nothing about\nwhether your agent credentials carry issuances. The renewal refusal above is the signal that does.\n\n### Which side to upgrade first in a split topology\n\nMove the manager first.\n\nThe stores 0.49.0 introduces are created by the **manager** at its own boot, not by the broker.\nThey are create-or-verify and idempotent, so a 0.49.0 manager brings the space's authority stores\nup to the new shape itself, and it does so against whichever broker is answering.\n\nBeing honest about the evidence behind each direction, because they are not equally established:\n\n- **Broker-first was measured on a live 30-agent deployment** (issue #1578). Upgrading the broker\n first locks the old manager out immediately: `cotal up` re-renders the broker's generated config\n from the trust record, and after the restart the still-0.48.2 manager is refused on every\n connection with an `authentication error` naming the Nkey, continuously. That text comes from the\n broker process, not from a Cotal command, so match on its shape rather than on an exact string.\n `cotal ps` reports zero agents while\n the agent processes are still alive, because the manager has lost its view of them, not because\n they died. Upgrading the manager clears it immediately.\n- **Manager-first is reasoned from where the new stores are provisioned**, not from a measured\n fleet upgrade. It is the recommended order because the manager is the component that creates what\n 0.49.0 adds, but it has not been run end to end on a production split topology at the time of\n writing. Treat it as the better-supported order rather than a guaranteed one, and keep the\n rollback below ready either way.\n\nWhichever order you pick, **this is not a rolling upgrade**. Between the two steps the mesh is down\nand the manager cannot see its agents. Go straight through rather than pausing between them, and\nschedule it as an outage window.\n\n### What the window looks like\n\n- **The managed agent processes do not survive step 1, in either order.** This is the one place\n where the obvious reordering does not rescue you, so it is worth understanding rather than\n working around. Sparing agents on a bare manager stop is a **handshake**: a 0.49.0 manager\n publishes a capability file proving it can release its agents, and a 0.49.0 `cotal down` refuses\n the stop unless it finds one. **A 0.48.2 manager never publishes that file**, because the\n mechanism ships in the release you are installing. So the old CLI against the old manager sends a\n plain stop and takes every seat with it, and the new CLI against the old manager either refuses\n (leaving `--with-agents`, which reaps deliberately) or falls to the legacy path, warns that it\n cannot verify the manager can spare its agents, and signals it anyway.\n- **You can confirm which side you are on in one command, without stopping anything.** The flag that\n marks the newer behaviour is absent from the older CLI, and its summary line makes the difference\n plain:\n\n ```\n $ cotal down --help # on 0.48.2\n cotal down - stop the whole local stack, or name only the components to stop\n\n $ cotal down --help # on 0.49.0\n cotal down - stop the whole local stack (managed agents stay running unless --with-agents), ...\n ```\n\n If your `cotal down --help` does not mention `--with-agents`, stopping the manager stops the\n agents with it.\n- **Therefore the respawn in step 5 is mandatory recovery for this upgrade, not an optional pass.**\n It is also the step that re-mints credentials as issuances, so it is the same action either way.\n Plan the window to include it rather than treating it as cleanup.\n- The **manager's view** of them is lost while the two sides disagree, so `cotal ps` reports zero\n and control commands do not reach seats.\n- **Messages are not delivered** while the mesh is down.\n- The window is as long as it takes to restart the second component, plus the manager's own start.\n It is minutes, not hours, provided you do not stop between the steps.\n- **Nothing self-heals if you stop halfway.** The refusal is continuous until both sides match.\n\n### Snapshot this before you start\n\nTake these while the deployment is still on 0.48.2. The two `cotal` reads are live reads and must\nhappen before anything stops.\n\n- **A filesystem or volume snapshot of both containers**, if your platform offers one. This is the\n only rollback that covers every case, and it is what the reporting deployment used.\n- **`cotal backup create <dir>`**, for the durable space state, **but read the next paragraph before\n you rely on it**: on a split broker and manager topology it is very likely unavailable to you, and\n the volume snapshot above is your actual rollback.\n- **The trust records and credential directory** under `.cotal/auth` on the manager host, including\n the per-space material directory. These are what a re-mint would otherwise have to replace.\n- **A copy of the channel registry**, so you can verify it came back rather than assuming it did:\n `cotal channels list` before and after.\n- **The output of `cotal ps`**, so you know how many seats you expect to see afterwards and can tell\n a lost view from a lost agent.\n\n#### `cotal backup` on a split topology\n\n**`cotal backup create` cannot read a running stack.** It requires a completed cut, and only\n`cotal down --preserve-state` publishes one:\n\n```\n$ cotal backup create ./backup.0482\n\u2717 backup requires a completed cut; run `cotal down --preserve-state` first\n```\n\n**And `cotal down --preserve-state` requires a manager alive on the host you run it from.** It uses\nthat manager to attest that every retained child stopped, and the check is deliberately fail-closed:\na manager that is dead or merely uncertain refuses rather than preserving an unproven cut. The check\nreads a local pidfile, so a **remote** manager does not satisfy it. On a split topology the broker\nhost has no local manager, which means the documented durable-backup path is not available there.\n\n**Measured rather than assumed, at 0.48.2**: the backup refusal above is executed output. The\npreservation requirement is read from `down.ts` at the same tag, where the preserve path asks a\nmanager to prepare an inventory and then requires that manager to be locally alive before it\ncommits. The part not executed end to end is a genuine two-host split, which needs two real hosts.\n\n**What to do instead.** Use the filesystem or volume snapshot of both containers. That is the\nrollback the reporting deployment actually used, it covers the broker's durable state and the\nmanager's credential material together, and it does not depend on either component being able to\nattest for the other. If you want `cotal backup` as well, take it from a host that does have a live\nlocal manager, and understand it is a second copy rather than the primary rollback.\n\n**This looks like a product limitation rather than a documentation gap**, and it is written here as\none so an operator is not left thinking they mis-typed a command. The upgrade path for the exact\ntopology this page is addressed to cannot use the documented backup command.\n\n### The upgrade end to end\n\n```bash\n# 0. on 0.48.2, STILL RUNNING: record what you expect to see afterwards.\n# These two are live reads, so they must happen before anything stops.\ncotal channels list > channels.before\ncotal ps > ps.before\n\n# 1. manager host. READ THE NOTE BELOW THE BLOCK FIRST: this step ends the\n# managed agent processes whichever order you choose, and the respawn in\n# step 5 is how they come back. It is recovery, not tidying.\n#\n# STOP THE MANAGER WITH THE 0.48.2 CLI, BEFORE INSTALLING 0.49.0. The\n# order matters and it is not recoverable once you install: a 0.49.0\n# `down manager` REFUSES to stop a 0.48.2 manager whose pid record carries\n# a start token, which is every manager on a platform that can read one\n# (Linux can):\n# refusing bare manager stop: ... does not prove this manager can detach\n# its agents; use --with-agents or stop the agents explicitly\n# The refusal names two remedies and NEITHER clears it for this case. The\n# check reads a capability file that only a 0.49.0 manager writes; it never\n# counts agents, so stopping them first changes nothing. And `--with-agents`\n# is whole-stack only, so `down manager --with-agents` is refused by its own\n# flag rule. See #1592.\ncotal down manager # the 0.48.2 CLI, still installed.\n # 0.48.2 has no --with-agents; this\n # is the whole route. On a host that\n # runs the whole stack, the 0.49.0\n # `cotal down --with-agents` after\n # installing is the alternative.\nnpm install -g cotal-ai@0.49.0 # ONLY after the stop above\n# `supervise` RUNS IN THE FOREGROUND and holds the terminal until you stop\n# it. There is no --detach on this command. Start it under whatever keeps\n# your manager alive normally (systemd unit, container entrypoint, or a\n# second terminal), and run the remaining steps from another shell.\ncotal supervise --space <space> --server nats://<broker>:4222\n\n# 2. broker host: stop the stack.\n# NOT `--preserve-state` on a split topology: it needs a manager alive on\n# THIS host to attest its children stopped, and yours is on the other one.\n# Your rollback is the volume snapshot from \"Snapshot this before you\n# start\", not `cotal backup`.\n# See \"cotal backup on a split topology\" above.\ncotal down\n\n# 3. broker host: install 0.49.0 and start it again\nnpm install -g cotal-ai@0.49.0\n# Record the manager log's size BEFORE starting, so step 3a can tell THIS\n# boot's output from every earlier one. It must be captured here, ahead of\n# the start: taken afterwards it sits past the new line and the wait hangs.\n# `<spaceKey>` is NOT the space name. It is lowercase hex of the name's\n# UTF-8 bytes, so space `prod` is `manager.70726f64.log`. Do not guess it:\n# `cotal up` prints the real path on its launch line. Substituting the\n# plain name points at a file that does not exist, and the wait below then\n# burns its full timeout before telling you.\nLOG=.cotal/manager.<spaceKey>.log\nOFF=$( [ -f \"$LOG\" ] && wc -c < \"$LOG\" || echo 0 )\ncotal up --detach --host 0.0.0.0 --space <space> --no-manager\n\n# 3a. SPLIT TOPOLOGY ONLY: `--no-manager` above boots the broker (and the\n# delivery daemon) with NO local manager on the broker host, so there is\n# no wait-and-stop step on a current cotal-ai. The rest of this step is\n# the OLDER-host recipe, kept because the flag is refused there and that\n# refusal is your signal you are on it: without the flag the `up` also\n# starts a local manager, and you must wait for the log to show it is up,\n# then stop it, or you finish the upgrade with two managers and the one\n# you did not intend is the one nobody is watching.\n# A bare `grep -q` does NOT wait: it reads once and exits 1 immediately\n# if the line has not been written yet. Bound the wait instead, so a\n# manager that never comes up fails loudly rather than reading as ready.\n# The log is opened APPEND-ONLY, so on any host that has run a manager\n# before, this file ALREADY carries a `manager up` line from an earlier\n# boot. Grepping the whole file therefore matches instantly and waits for\n# nothing. Read only what THIS boot appended, using the $OFF captured in\n# step 3 above (before the start, which is the only point it is correct):\ntimeout 60 bash -c \\\n \"until tail -c +$((OFF+1)) \\\"$LOG\\\" | grep -q '. manager up'; do sleep 1; done\"\n# exit 0 = THIS boot logged it; exit 124 = it never did, so STOP and look.\n# This manager is 0.49.0 and publishes its own spare-capability file, so\n# the bare stop below is NOT the refusal case from step 1.\ncotal down manager # broker + delivery remain\n# On a current cotal-ai the two commands above are unnecessary (nothing\n# to wait for, nothing to stop) and `cotal down manager` simply reports\n# no manager to stop.\n\n# 4. verify the mesh is whole again before touching the fleet.\n# Do NOT compare `cotal ps` against ps.before yet: step 1 ended the agent\n# processes, so at this point it is EXPECTED to be empty, and an empty\n# `ps` is also the signature of the broker/manager mismatch described\n# above. The two are indistinguishable here, so compare what the mesh\n# itself should have carried across instead:\ncotal channels list # compare against channels.before: this SHOULD match now\ncotal ps # expect it to be EMPTY here; ps.before is the target for\n # step 5, not for this step\n\n# 5. the step that is easy to skip: respawn the managed agents so their\n# credentials are re-minted as issuances and can renew. Persona is a\n# POSITIONAL argument here, unlike `cotal stop`, which requires --name.\n# One call per agent:\ncotal spawn <persona> --detach --name <n> --space <space>\n# then the comparison step 4 could not make:\ncotal ps # NOW compare against ps.before: seat count should match\n```\n\nThe mesh is down from step 2 until step 3 finishes. That is the window. On a split topology there is\nno cut and no backup inside it, so the window is the stop, the install and the restart, nothing more.\n\n## Adding a section for a future release\n\n**Every changeset marked breaking adds a section to this page.** A release that changes what an\noperator must do, in what order, or what stops working, is not finished until the section exists.\n`scripts/upgrade-section-gate.mjs` grades a commit range for this: run it as\n`pnpm upgrade-section-gate --base <ref>` and it reds when the range carries a breaking change and\nadds no new release section. The former Attribution workflow enforced this on each pull request;\nthat workflow and its required-status rule were removed on 22 October 2026. The script remains\navailable for manual checks, and reviewers still check upgrade instructions for breaking changes.\nBe precise about what the check proves. It proves a section for a release\n**was written here**. It cannot prove the section is **correct**, or that it describes the break\nthat actually landed, and it cannot see a breaking change that carries no marker at all. Reviewing\nthe words remains a person's job.\n\n**Mark the break, or the gate cannot see it.** Any one of these is enough, and they are the only\nthings it reads:\n\n- a `!` before the colon in the commit subject, as in `feat(core)!: bind hosted runs to the caller`\n- a `BREAKING CHANGE:` footer in the commit body\n- a changeset in `.changeset/` declaring a `major` bump for any package\n\nThe marker must survive the squash. A `!` that lives only in a commit you squash away is not in the\nrange the gate grades, so put it in the subject that lands on `main`.\n\n**The heading is a `##` and names the release**, like `## From 0.48.2 to 0.49.0`. Both matter, and\nneither is a style preference. Coverage is claimed by a heading, so a heading that names\nno release claims every release and distinguishes none: `## Notes` with a sentence under it would\notherwise satisfy the rule. Naming the release also makes the section the one an operator upgrading\nthat release will search for. Use `###` freely for detail inside a section. Subsections belong to\ntheir release rather than counting as separate coverage.\n\nName the release that first carries the change: the next version Changesets publishes, which\n`pnpm changeset status --verbose` lists. `bin/package.json` on `main` still reads the release already\npublished. If a release is cut while the change is open, the change ships in the release after it,\nso move the heading before merging. The gate accepts any version in a heading, so before merging a\nrelease pull request, check every heading added since the previous tag against the version it\npublishes.\n\nA section is written for the operator, not for the reviewer. It answers, in this order:\n\n1. What keeps working with no action at all.\n2. What does **not** migrate, and when that becomes visible. Name the log line if there is one.\n3. The order to move components in for a split topology, and why that order.\n4. What the outage window looks like, including what survives it.\n5. What to snapshot before starting.\n6. The commands, end to end.\n\n**Where an answer was not measured, say so in the document rather than guessing.** An operator who\nknows which half of a recommendation is reasoned and which is measured can plan around it; one who\nfinds out afterwards cannot.\n\n## Handle redemption in 0.91.0\n\n`auth redeem <private-handle-file>` selects the native auth target, then refuses unsupported\nconferring-handle redemption without reading the file or issuing credentials. It does not redirect\nthe artifact into agent enrollment or interactive-session redemption. Offline handle inspection\nstill labels signatures, revocation and holder currency not checked. An explicit `--space`\nrequests static-registry signature inspection with a native ephemeral operator reader. It checks\nsignatures, issuer scope, presented links and required revocation records without establishing\npresenter identity, live epochs or use authority. It confers no holder credential, persists no\nreader credential, and never turns missing or stale native records into verified offline success.\nOpen/user-auth inspection and conferring redemption remain unsupported.\n\n## Dashboard service transition in 0.91.0\n\nThe matching dashboard package now registers a service preset and hidden native child, not a\npublic web command. An older cached command descriptor is hidden and refuses with the matching\nextension-update remedy before importing provider code or repairing the install. `update` remains\nexplicit; `service start web` never installs a deliberately absent extension.\n\nConfigure the saved web definition with its existing registered static-auth root, one protected\ncredential file and explicit read-only intent. The service retains required TLS, creates no caller\nor channel-purger credential, opens no browser and prints no bootstrap secret into logs. The page\nmarks it read-only and hides delete; the server independently rejects that mutation before any\nalternate connection. The private session file holds the full-launch bootstrap; logs give its\nreference only. User-auth/open targets and OS installation remain unsupported in this route.\nExisting legacy process records are still observed, never silently adopted. An already-running\nlegacy dashboard is not restarted or converted by loading metadata. See [dashboard setup](watch-a-mesh.md#web-dashboard).\n\n## Registered lifecycle retirement in 0.91.0\n\n`auth retire <full-lifecycle-uid> [--yes]` has closed UID/space input, but the standalone CLI still\nlacks a supported native retirement instrument carrying its own current serve identity and gate.\nIt refuses without publishing a request. It does not borrow the manager's identity, treat --yes as\na grant, or call the host's interactive/managed loopback doors as a substitute. This remains an\nexplicit authority gate, not a completed retirement operation.\n\n## Local-process `rootedAt` removal in 0.91.0\n\nA local-process extension descriptor no longer has a `rootedAt` field. No stop has read it since\nthe dashboard became a service. Operators have nothing to migrate, snapshot or order, and there is\nno outage: a manifest that an older install cached with the field still loads, and the next\nmanifest rewrite drops it. An extension author removes the field from each `LocalProcess`\ndeclaration, where TypeScript now reports it as an unknown property.\n"
|
|
65479
|
+
"body": "# Upgrading a running deployment\n\n> **Guide** (informative) \xB7 **For:** operators upgrading a mesh that already exists \xB7 **See also:** [Substrate stability](stability.md), [Run a mesh](run-a-mesh.md), [Identity and auth](identity-and-auth.md)\n\n[Substrate stability](stability.md) tells you what the version numbers promise. This page is the\nother half: what to actually do when the deployment already exists, has credentials in it, and\ncannot simply be recreated. Every release that breaks a running deployment gets a section here,\nnaming what migrates on its own, what does not, and the order to move the pieces in.\n\n## The pre-1.0 upgrade contract\n\nThe packages are pre-1.0, so a minor bump may break an API or an on-disk expectation. Four\ncommitments make that survivable for someone with a fleet:\n\n- **Pin an exact version.** `0.N.P`, never `^0.N.P`. A range can pull a breaking minor in during an\n unrelated reinstall.\n- **Every break that touches a running deployment gets a section on this page**, written in terms of\n what an operator does, not in terms of which module changed.\n- **Read the section before you start, not halfway through.** A section names the work up front\n precisely so the operation does not change shape once it is underway.\n- **A break that cannot be made automatic says so.** Where credentials or state must be recreated by\n hand, the section says which ones and when, rather than leaving you to discover it at the moment\n the first one stops working.\n- **A change to the shape of a credential, or to who may renew one, is breaking whatever the commit\n marker says.** This rule is stated because the marker is a judgement made while writing the code\n and the consequence is felt by someone running it a day later. A fleet that keeps authenticating\n looks compatible and is not, if nothing in it can renew. Any automated check of this rule would\n read commit markers, so a break recorded as a feature is the one case it could not see, which is\n why the rule is written for people first. **The marker held for this release: the 0.49.0 change\n that caused all of this, `36d177951 feat(core)!`, did carry its `!`.** The rule exists for the\n next one that does not.\n\nWhat this page does not promise is a rolling upgrade. Nothing in the current line dual-serves two\nauthority versions, so where broker and manager run separately there is a window in which the mesh\nis down. The sections below give that window's shape so it can be scheduled rather than endured.\n\n## Personal native connections in 0.92.0\n\nUpdate the CLI and its Claude plugin together, then reopen the native Code session. Existing static\nauth connections keep their credentials and gateway tools. Personal signed-in connections are new;\nthe plugin binds a native conversation through its ordinary command hooks before creating one.\nThis works without intercepting the `classic.*` events protected by Claude's built-in guard; keep\nthe guard enabled. Organization policies that disable command hooks or native modules still apply.\nRemote meshes must advertise\nconversation identity v1 on the selected registration's HTTPS origin. The existing account signs\nin through Cotal; do not copy login files or paste a token into Claude.\n\nEach signed-in conversation gets its own roleless actor, direct messages and an own-event-only\npublication grant. The space's required-event policy remains in force. Desktop Chat cannot use this\nnative path because it has no Code transcript binding.\n\nCreation persists the request and secret before contacting the authority. Retrying an interrupted\ncreation reuses them. Disconnect can report pending cleanup; retry it for the same connection.\nPrivate recovery state remains until the authority confirms that exact lifecycle is retired.\nNeither an upgrade nor a lost response silently creates replacement authority.\n\n## Cooperative workflow cancellation in 0.92.0\n\nThe manager adds the issued, instance-bound `run-cancel` command. Its cluster revision advances to\n37, retaining the vitals and keyed-submission commands. Cancellation is available through the\ngeneric endpoint client; this change adds no CLI or MCP cancellation shortcut.\n\n### What keeps working\n\nExisting run records, admissions, revocation markers and journals keep their formats. Upgrade does\nnot rewrite them, cancel a run or remove a marker. A released run without a marker can still resume\nunder its admission. A revoked run remains revoked. Manager shutdown still requests boundary\nrelease; it does not write a cancellation marker or prove an agent process stopped.\n\n### What needs an update\n\n- Custom `RunHost` implementations must return a drive with `cancel(reason)`, in addition to\n `done` and `release(reason)`. It must propagate cooperative root cancellation to in-flight\n effects. Do not substitute boundary release or process termination. Rebuild and qualify the\n custom host against the new core contract before enabling it on an upgraded manager.\n- The built-in runtime and language packages carry the root signal through both engines and the\n compiled worker. Keep core, auth, manager, runtime and language on the same release; copying one\n source file or replacing only the manager does not supply the new protocol and worker code.\n- Existing caller credentials do not gain a new publish permission on installation. Obtain fresh\n issued credentials through the supported issuer before using `run-cancel`. Static credentials\n and caller generations must still meet the original lifecycle and grant checks. Never edit a\n credential to add the permission.\n- The new command requires a fresh Describe-derived instance and epoch bind. An old issuer lacks\n its independent request observer and inspect/commit path, so a new signerless manager cannot use\n that issuer for cancellation. Do not treat missing support as a reason to bypass the issuer.\n\n### Order for a split topology\n\nSchedule a maintenance window for the version change; this is not a rolling-compatibility promise.\nTake the deployment's usual consistent backup of broker state, including run records, admissions\nand journals, and the issuer's private configuration. Upgrade the issuer, then the manager and its\nruntime dependencies to the same release, using their normal stop/start procedure. Reinstalling a\npackage does not replace modules already loaded by a running process. Reconnect callers through\nthe upgraded issuer and resolve the manager's current contract before invoking cancellation.\nThere is no broker format migration for this command and no automatic cancellation during upgrade.\n\n### What the result establishes\n\nA durable marker denies future admission but is not proof of cessation. Only `stopped` with the\ncaptured drive outcome establishes that local drive's result. If the holder recheck fails or the\nrequest budget expires, a parked effect can remain running beside a committed marker. Preserve an\nunknown result; do not blindly resend it or declare cleanup complete. To continue cancellation,\na caller can make a fresh explicit request, which observes the marker and current holder before\nreporting its own result. Pending cancellation intent can also block a local launch after a definite\nno-dispatch refusal if marker absence was not confirmed within the budget.\n\n`run show` now displays revocation independently of recorded state. An unreadable marker yields\na partial result and exit 4 in human and JSON output, rather than an apparently complete view.\nAutomation must handle that existing partial-result convention. None of these observations proves\nmodel or tool preemption, physical process cleanup or cessation on another host.\n\n## Service authority status selector in 0.92.0\n\nA service endpoint authority status query that reads one operation now names that operation:\n`target: { kind: \"operation\", operation, requestId }`, where `operation` is `register`,\n`activate`, `renew`, `ready` or `revoke`. `@cotal-ai/core` exports the operation names as\n`ServiceStatusOperation` and the whole selector as `ServiceStatusTarget`. The 0.91.0 selector,\n`{ kind: \"operation\", requestId }`, no longer parses. The change is in `parseServiceAuthorityRequest`,\n`parseServiceAuthorityEnvelope` and the `ServiceAuthorityRequest` type, so it reaches every client\nand server built on them.\n\nA `revoke` operation status parses, but the 0.92.0 auth service does not answer it yet: it is\n`denied` with reason `unsupported` (HTTP 422), as SPEC states. To follow a revocation, read\n`target: { kind: \"instance\" }`, which reports `revoking` with the revoke's operation id.\n\n### What keeps working\n\n- Every running mesh. 0.91.0 shipped the request types and the client (the auth provider's\n `serviceEndpointAuthority` hook and its HTTP client), but no 0.91.0 auth service serves\n `/service-endpoint-authority`, so no deployment has status traffic on this route to lose. In\n 0.92.0 only a context opened with `serviceEndpoints` serves the route. That option is off by\n default and has no CLI flag; without it the route answers 404.\n- `target: { kind: \"instance\" }` is unchanged, and so are the register, activate, renew, ready and\n revoke requests themselves.\n- No credential, broker or connection is involved. Nothing needs reminting and no broker restarts.\n\n### What does not migrate\n\n- **Code that builds an operation status query without `operation`.** The 0.92.0 provider hook and\n client parse each request before sending it, so the query fails locally with a `bad-request`\n error, `service-endpoint authority: <detail>`, whose detail is\n `object has missing or unsupported fields`, and nothing is sent. An operation name outside the\n five gets the detail `unsupported status operation`.\n- **A 0.91.0 client still sends the old selector.** A 0.92.0 auth service opened with\n `serviceEndpoints` answers it with HTTP 400, whose body carries the same parser message. The\n service reads and writes nothing for it and logs nothing. The 0.91.0 provider hook and client\n never show that message: they cannot parse the 400 body as a protocol result, so they throw a\n `ServiceAuthorityTransportError` whose message is\n `service authority returned an unconfirmed response; retain the existing request identity`, with\n `httpStatus` 400 and outcome `not-executed`. A status query is a read, so nothing ran and\n nothing needs replaying. An auth service without `serviceEndpoints` answers 404 before it reads\n the body.\n- **No legacy operation is inferred.** Neither the client nor the service maps an old selector onto\n `register` or any other operation. One requestId can key a different request under each\n operation, so a guessed operation could answer with another request's status. Send the operation\n the request was made as; the requestId you stored with it stays the same.\n\n### Durable data\n\nNothing stored changes on upgrade, and nothing is rewritten. The selector is a request shape and is\nnever stored. 0.91.0 wrote no service authority request intents or operation records, so there is\nnothing to convert. A 0.92.0 context opened with `serviceEndpoints` writes its own records from the\nmoment it is enabled: an admission record when it first opens, then request intents, the\nper-instance intent index, operation, proposal and assignment records, and endpoint gates. Each\nintent already records its operation. Without `serviceEndpoints` it writes none of them.\n\n### Order for a split topology\n\nUpgrade every client that builds status queries together with, or before, the first auth service\nyou open with `serviceEndpoints`. That means code using `ServiceAuthorityRequest` or\n`parseServiceAuthorityRequest`, and every process that calls the auth provider's\n`serviceEndpointAuthority`. This change adds no ordering for brokers, managers or connectors;\norder those as their own sections say.\n\nThere is no compatibility window to schedule. A 0.92.0 auth service never accepts the old selector,\nand a 0.91.0 auth service has no handler for the route at all, so a mixed pool is not partly\nworking: every 0.92.0 auth service opened with `serviceEndpoints` refuses an old client's\noperation status queries with 400, which that client reports as the unconfirmed-response error\nabove, and one without `serviceEndpoints` answers 404. Nothing else is interrupted, so the change\nhas no outage window of its own.\n\n### Before the upgrade\n\n- Nothing needs a snapshot for this change, because it writes nothing. If the same upgrade first\n enables `serviceEndpoints`, take the deployment's usual consistent backup of broker JetStream\n state and the auth host's private configuration before that step, because the service endpoint\n host then starts writing to the auth bucket.\n- Find the old selectors in your code, then name the operation in each:\n\n```sh\ngrep -rn 'kind: \"operation\"' --include='*.ts' --include='*.js' src/\n```\n\n```ts\n// 0.91.0\n{ ...coordinates, operation: \"status\", target: { kind: \"operation\", requestId } }\n// 0.92.0\n{ ...coordinates, operation: \"status\", target: { kind: \"operation\", operation: \"register\", requestId } }\n```\n\nWhat this section measured, and what it did not: the local refusal and the accepted and refused\nselectors are covered by the core parser's own test. The 400, 404 and `unsupported` answers, and\nthe absence of a log line, are read from the auth service's source. The 0.91.0 client's error is\nread from the 0.91.0 client and core parser source; the client file is unchanged since 0.91.0. No\nmixed-version deployment was run.\n\n## History reader deletes after 0.86.0\n\nObserver and admin credentials now carry a `CONSUMER.DELETE` row for each of the connection's eight\nhistory-reader names, `hist_<readerId>_<n>`, on CHAT (and on DM for admin). `channelHistory`,\n`multiChannelHistory` and `dmHistory` read under those names and delete each reader when its step\nends. Before this, every read left its consumers for five minutes, and a dashboard polling history\nfilled the stream's consumer limit until reads were refused with \"maximum consumers limit reached\".\n\n### What stops working\n\nA static observer or admin credential minted by 0.86.0 or earlier has no reader-delete rows. On it,\na current client's history read fails with \"it predates the observer and admin reader-delete grant,\nso remint it\", and leaves one reader per stream until the broker's five-minute inactive threshold.\nA credential the CLI mints at start (`cotal console`, `cotal web` on a static auth mesh) is current\nafter the upgrade.\n\n### Before the upgrade\n\n- Remint every observer or admin credential a process reads from a file: `cotal auth mint <name> --profile\n observer` or `--profile admin`, and any `--creds` file given to a dashboard.\n- On a user-auth mesh, the auth service mints these views per connection. Upgrade it before, or with,\n the hosts that read history, so no current client connects through an older auth service.\n\n## Native key issuance recording after 0.85.0\n\nBefore upgrading, take a consistent backup of broker JetStream state and the auth host's private\nconfiguration and signing material through the deployment's supported backup procedure. The auth\nbucket must use file storage. Allow space for one issuance record per new callout exchange until\nits minted expiry plus clock tolerance; static credentials have no such record.\n\nEmbedders that call `startAuthCallout` must supply the required `recordIssuance` callback. It must\nfinish a durable create-only write for the broker-generated user key and recheck the validated\nlifecycle and credential authority before returning. A missing callback refuses startup; a failed\nwrite or recheck denies the exchange. Do not use the isolated tests' no-op callbacks in a deployment.\nThe stock `startAuthService` authority plane wires the recorder and its pruning automatically.\nExisting auth records need no manual rewrite, and no tenant or participant grant is added.\n\nFor split deployments, pause new managed launches and stop every old auth-callout responder before\nstarting the matched new auth service. A mixed responder pool can still issue keys without records.\nNew connections and reconnects cannot authenticate while the callout service is down. Existing\nbroker connections keep their current JWT expiry; this change needs no broker restart. Upgrade the\nCLI, Manager and connector packages together before admitting new managed launches, using the\nruntime's supported Manager custody or recovery path. This source change does not promise that\nrunning seats survive a Manager restart.\n\nThe new service prunes expired issuance rows before opening and on a timer. A corrupt row refuses\nopening or fences the running authority plane; investigate the stored evidence through the normal\noperator recovery procedure. Do not erase the auth bucket or recreate signing keys to clear it.\nRows written before a later authority-check failure can remain until pruning; they do not prove\nthat a client received a JWT or established a connection.\n\nConnections admitted before the last old responder stopped, static JWTs and old responders have no\nissuance record. They are not backfilled or identified from names. Bearers without current lifecycle\nand credential claims cannot be recorded and are refused; use the supported enrollment or renewal\npath for the selected release. These records alone do not provide persistent native-key cutoff or\ncomplete revocation. Reintroducing an old responder also reopens the recording gap.\n\n## Native Claude MCP discovery after 0.82.2\n\nBefore upgrading, snapshot Cotal configuration and the Claude profile on each launch host.\nOrdinary Claude spawns now load the host's native MCP sources alongside declared Cotal overrides.\nAn empty sharing map no longer suppresses native servers. Audit enabled servers and their\ncredentials before admitting new seats. Use an explicit sharing selection, including\n`--share-tools none`, to retain isolation. Existing sessions keep their loaded configuration.\n\nNo broker restart, credential-format migration or cross-host profile copy is required. Upgrade\nthe CLI, core, manager and connector together on each launch host. Pause new managed spawns\nwhile adopting the matched packages; use the runtime's supported manager custody or recovery\npath. Any interruption is that manager transition, not a change to the broker wire contract.\nCarried private homes stay isolated and do not import the operator's native profile.\n\n## Connector-store protection after 0.82.2\n\nSnapshot the private Cotal state, connector store and mesh configuration before upgrading.\nInstall the new CLI on each CLI and manager host, then verify the selected executable before\nrunning setup or store maintenance. The POSIX installer prints its absolute launcher because\nit cannot change the parent shell's command cache, aliases or functions. An older executable\nalready on disk remains unsafe for a newer store; do not use its force advice to recover.\n\nForward setup and connector seeding migrate built-ins automatically. Same-generation force and\nreset rebuilds remain supported. An executable older than the store now refuses every backward\nmigration, including force and reset. Select a verified executable at least as new as the store\nand run normal seeding instead. No credential or mesh-record format needs manual migration.\n\nA split deployment needs no broker restart for this change. Keep each host's CLI and connectors\non the same release. Pause new managed spawns while adopting it; a running manager keeps its\nloaded code until restarted. Use the chosen runtime's supported custody or preserve-state path.\nThe interruption covers that manager transition and depends on its runtime, not a broker outage.\nDo not overwrite a live manager installation or stop an unidentified listener to force adoption.\n\nAn implicit foreground spawn now chooses a numbered actor when the requested actor is occupied.\nExisting grants, launch material and sessions stay in place. An explicit name remains pinned\nand refuses an occupied actor rather than renaming it. Signed enrollments and managed handoffs\nkeep their existing identity semantics.\n\n## Background startup in 0.91.0\n\n`cotal up` now returns after background startup by default. `cotal up --detach` retains that\nmeaning. Use `cotal up --foreground` when terminal lifetime should own the native stack.\n\nA systemd unit, launchd KeepAlive job, container entrypoint or script that supervises the `up`\nprocess must add `--foreground` before upgrading.\n[Persisted callers](#persisted-callers-in-0910) gives the literal\nreplacement for each.\n\nA project folder an earlier release started keeps its own launcher after the upgrade. Stop it with\n`cotal down` as usual, upgrade, then run plain `cotal up` in that folder: it starts on the retained\nlauncher with the same identity, store and history. It does not need `space create` or `space add`.\n\nRecorded recovery requests retain their original foreground/background intent. They do not\nreinterpret a prior request using the new default. This change does not migrate a whole stack to\nowned services.\n\nPrepared plain-loopback user-auth roots can use the controller's broker, auth service, delivery and\nmanager members. First enable may return a known partial result after the first three start: the\nmanager needs an existing signed-in, granted CLI login. Sign in and obtain that grant, then run\nbare `cotal up`; no static caller or new grant is substituted. Auth endpoints come from the\ncreation-pinned child's native provider readiness, not a guessed URL. The controller keeps unknown\noperations blocked. Existing three-member auth/open journals are read with an unattempted auth row\nand written with all four roles; old clients do not understand that new record shape. Upgrade the\ncomposed CLI together before using it. TLS-required, remote and public-exchange user-auth paths\nretain their native route.\n\n## Persisted callers in 0.91.0\n\nSome command lines outlive the install that wrote them: OS units, scripts and container commands.\nAfter `npm i -g cotal-ai@<new>` they reach the new binary before any `cotal` command has run. This\nsection says what each one does after the upgrade and what to change before it.\n\n### Command mapping\n\n| Earlier command | Command in 0.91.0 |\n|---|---|\n| `meshes`, `meshes add` | `space`, `space add <name>` |\n| `clean store`, `clean all` | `reset store <space>`, `reset space <space>` |\n| `mint <name>` | `auth mint <name>` |\n| `mint --signer` | `auth signer --out <private-file>` |\n| `actor list`, `actor grant <actor>`, `actor revoke <actor>` | `auth grants`, `auth grant <actor>`, `auth revoke <actor>` |\n| `join [channel]` | `chat [channel]` |\n| `console` | `watch` |\n| `channels` | `channel` |\n| `send dm <agent>`, `send msg <channel>`, `send ask <role>` | `send -t <agent>`, `send -c <channel>`, `send -r <role>` |\n| `personas` | `persona` |\n| `endpoints`, `describe`, `invoke` | `endpoint`, `endpoint describe <target>`, `endpoint invoke <target> <command>` |\n| `deregister-instance` | `endpoint deregister <class> <instance-id>` |\n| `reconcile-gate`, `seats` | `manager reconcile`, `manager seats` |\n| `topology view` | `team check <file> --graph` |\n| `down -f <manifest>` | `team remove <deployment-id>` |\n| `status --components` | `service` |\n| `run journal`, `run migrate`, `run ps` | `run show`, `run check`, `run` |\n| `deliver pending <name>` | `pending <agent>` |\n| `web` | `service add <name> --preset web`, then `service start <name>` |\n| The four daemon entries | Their service presets, under [deprecated daemon entries](#deprecated-daemon-entries) |\n\nThe sections on this page for each command family give the details of its change.\n\n### What stops working\n\n`cotal up` returns after background startup by default. An earlier line such as\n`cotal up --space <space> --server <url>` that a supervisor runs as its main process now exits\nonce the stack is started. systemd then treats a `Type=simple` unit as finished and can stop the\nprocesses that unit started, and a restart policy starts another attempt. A launchd `KeepAlive`\njob relaunches it the same way. Add `--foreground` before upgrading. For a systemd unit, replace\n`ExecStart=/usr/bin/cotal up --space <space> --server <url>` with:\n\n```ini\n[Service]\nType=simple\nExecStart=/usr/bin/cotal up --foreground --space <space> --server <url>\n```\n\nFor a launchd job, add `--foreground` after `up` in `ProgramArguments`:\n\n```xml\n<key>ProgramArguments</key>\n<array>\n <string>/usr/local/bin/cotal</string>\n <string>up</string>\n <string>--foreground</string>\n <string>--space</string>\n <string>main</string>\n <string>--server</string>\n <string>nats://127.0.0.1:4222</string>\n</array>\n```\n\nA container command or script makes the same change to its `up` line. When background `up` is the\nmain process of a systemd unit, it prints a warning naming the unit and the `--foreground` remedy\nafter it starts or refreshes the stack. `up -f`, which has no foreground form, and a dry run print\nnothing. A launchd job gets no warning. `cotal up --detach` keeps its meaning. The warning changes\nno mode and no unit, and no unit or script is rewritten or restarted.\n\n`cotal spawn <name>` now launches through a manager and attaches. A container or script that ran\nan agent directly with `spawn <name>` adds `--foreground`.\n\n### Deprecated daemon entries\n\n`supervise`, `deliver`, `auth-service` and `feedback-intake` stay accepted with their existing\nflags for every caller: units Cotal installed, units and scripts you wrote, and container commands.\nThey are deprecated and are removed in `1.0.0`, one release for all four. Help and completion no\nlonger list them. Each run prints one line on stderr naming its replacement and that release.\nStdout, behavior and exit codes are unchanged, so no unit restarts in a loop because of the\nupgrade. `deliver pending` warns the same way and names `cotal pending <agent>`.\n\n| Entry | Replacement |\n|---|---|\n| `supervise` | `cotal service add <name> --preset manager` |\n| `deliver` | `cotal service add <name> --preset delivery` |\n| `auth-service` | `cotal service add <name> --preset auth-service` |\n| `feedback-intake` | `cotal service add <name> --preset feedback-intake` |\n\n`cotal service add <name> --preset <preset> --help` lists the options a preset takes, and\n`cotal service start <name>` starts it. A `supervise --roster <file>` line maps to the manager\npreset's `--roster <file>`, with an absolute path, and `cotal service start <name> --foreground`\nkeeps the manager in the foreground as `supervise` did. The manager, delivery daemon and auth service that `up`\nstarts itself do not warn, so the line in a log always comes from a unit, script or container you\ncan change.\n\n### Owned units\n\nA manager unit that an earlier `cotal service install` wrote keeps calling `supervise`. After the\nupgrade it starts the new binary and prints the deprecation warning on each start. This release\ndoes not rewrite that unit.\n\nA unit that this release's `cotal service install <name>` writes runs the hidden `__service-unit`\nentry, which an earlier binary does not have. Before you downgrade, stop it with\n`cotal service stop <name>` and remove it with `cotal service uninstall <name>`. After the\ndowngrade, install the unit again with the earlier binary's `service install`.\n\n### Containers\n\nThe `solo` service in `deploy/docker/compose.yaml` runs `spawn scout --foreground`. The team\nservice and the image's default command still run `supervise` with `--roster`, so their logs show\nthe deprecation warning. An image you already built carries its own CLI and is unaffected until\nyou rebuild it.\n\n### Before the upgrade\n\n- Add `--foreground` to every unit, launchd job, container command or script that supervises\n `cotal up`.\n- Add `--foreground` to every container command or script that runs `cotal spawn <name>` directly.\n- Move units and scripts that call `supervise`, `deliver`, `auth-service` or `feedback-intake` to\n their service presets before `1.0.0`.\n- Before a downgrade, stop and uninstall every unit this release's `service install` wrote.\n\n## Team commands in 0.91.0\n\nThe new grammar names fresh intent `team boot <file>` and additive intent\n`team deploy <file>`. `team check <file> --graph` resolves declared access\nwithout a broker. `team` and `team show <id>` read local fresh summaries, additive ledgers and\nagent/channel returned receipts; `team remove <full-id>` is the ID-only additive\nteardown name. An edited manifest is not authority to remove an old deployment.\n\nBoot's `--dry-run` is a declaration view.\nDeploy's `--dry-run` reads native presence and channel registry for advisory\nclassification against a prior ledger. Remove's preview compares a legacy\nledger with native manager PS and display-only membership observations, or a\nreturned channel-only receipt with current KV revisions. No preview authorizes\na launch or stop. Bare `team boot <file>` enters guarded native fresh\nstartup only for a prepared, stopped local root; it refuses a leftover manager\nrecord and offers rich, JSON and plain receipts. User-auth boot records an intent before\nowner/trust preparation and retains uncertainty without retry. Windows boot\napply refuses, because protected service-record inventory is not supported there.\nBare `team deploy` applies all-new channel-only manifests with explicit live\ndelivery, or one to 100 all-new agents on a static/open mesh, optionally with\nup to 100 new live channel cards. A mixed deployment records one intent before\ncreating channels and launching agents through one pinned manager/runtime.\nEach channel revision and original launch request, allocation and matched join\nis recorded before continuing. A full receipt requires both sequences to match.\nUnknown effects are retained, not replayed; completed effects are not rolled back.\nBoth routes offer rich, JSON and plain output. Identical settled deployments can return a verified\nno-op without new effects. Named stale-agent waivers retain the old policy without restarting.\nUser-auth agent apply and existing-card mutation still refuse effects.\n\n`team remove <id>` can retire a returned static agent deployment through its original manager.\nReview `--dry-run` first; scripts require `--yes`. This needs the native slot query, original UIDs\nand native retired slot/head plus cleanup evidence, not just stop acknowledgements. A returned\nchannel-only deployment instead uses deletes conditioned on its original card revisions. A static\nmixed deployment retires agents first and needs a complete retirement receipt before checking and\ndeleting cards, under one confirmation and maintenance lock. Both receipts must exist before its\nblocker is released. Partial and unknown effects retain their journals without replay. History,\ngrants and readers are unchanged. Open/user-auth agent retirement and legacy removal still refuse.\nThe old `spawn -f` manifest spelling no longer deploys. Native `up -f` still\nstarts a fresh mesh; `down -f <file> --run <id>` remains the retained route\nonly for an older additive ledger. Move an automated agent deploy or teardown\nto the new spelling only when its auth mode and manifest are ones these routes accept.\n\n## Credential commands in 0.91.0\n\nCredential minting is `cotal auth mint <name>`. The old `cotal mint` spelling refuses before\nissuance and names its replacement. A stripped account signer is exported with the explicit\n`cotal auth signer --out <private-file>`, not `mint --signer`.\n\nThe default standing credential label is create-only. Repeated minting does not silently replace\nit. Choose a new label or a separate private output. An explicit existing output is replaced only\nwhen it is an owned private native credential matching the supplied `--identity`. Explicit\nexports cannot overwrite managed `.cotal` or workstation state.\n\nThe new receipt reports the actual principal, resulting lifecycle, private destination and\nnative expiry without credentials. Reusing an nkey does not promise reuse of its old lifecycle\nor bind-only inboxes. An agent-profile re-mint reports its newly allocated UID and warns about\nthat distinction; consuming clients must use the returned UID. Provisioning is explicit and is\nnot evidence that a client joined. Scripts should parse the finite `--json` document rather than\nscraping the former text lines. Secret and signer files must stay out of support bundles.\n\n`auth mint <name> --endpoint <class> --out <private-file>` is a separate caller mode. It uses the\nsame native static authority and a live digest-verified untargeted synchronous ephemeral surface.\nTargeted, journaled or action commands refuse the whole issuance; no unsupported row is dropped.\nThe native agent footprint and exact additional endpoint rows are provisioned with required TLS,\nwithout adding spawn/run/admin scope or starting a seat. `--channels` enters both explicit channel\nACLs and defaults empty; `--expires-in` remains native seconds.\n\nThe create-only private `<out>.issuance.json` intent is retained before footprint work and contains\npublic identity/lifecycle/scope metadata only. It is not a native grant or completion receipt. An\nexisting output or intent blocks replay, including when an interrupted operation left no output.\nKeep that intent with any original file while diagnosing an unknown result. Provisioner close is\nreported separately. No raw-file caller identity, managed launch, user-auth issuer or environment-\nexport onboarding is added by writing this credential.\n\n## Endpoint deregistration in 0.91.0\n\nReplace `deregister-instance --endpoint <class> --instance <id>` with\n`endpoint deregister <class> <full-instance-id>`. Both positional values are required.\nThe old spelling refuses before effects and has no implicit instance selection. Review the\nnew `--dry-run` output, then use `--yes` in scripts. JSON retains native revision receipts;\nunknown or superseded deletions are not retried. User-auth and open targets do not receive\na static-signing fallback.\n\n## DM pending inspection in 0.91.0\n\nUse `cotal pending <agent>` for a one-document JSON/plain read of native DM\ndurable counts and bounded candidate IDs. `--durable <exact-dm-durable>` targets\nan older lifecycle directly. The retained `deliver pending` spelling still\nuses its earlier output shape; scripts must migrate to `result` fields rather\nthan assuming the old per-line format. Candidate IDs are not proof of\nrecipient delivery or complete message history. This route has only the\nstatic/open admin read authority; user-auth has no corresponding view yet.\n\n## Live space deletion in 0.91.0\n\n`space forget <name>` changes only the local registration. It does not delete\nlive broker state. `space delete <name> --dry-run` now reads known first-party\nnative streams, bucket backing streams and presence entries on the registered\nbroker in open or static-auth mode. Static transfer-stream names are read under\nan additional exact-name grant on a second confirmed connection. This non-atomic\nread is not an apply plan or evidence that presence records are live. User-auth\ninspection and deletion remain unavailable. Bare deletion now requires a typed\nexact-name confirmation on a terminal or the exact positional name with `--yes`\nin a script. It rereads the native impact and checks the current target before\nsubmitting exact stream deletes, stopping on the first refused or unconfirmed\nreply with acknowledged and remaining names. A native post-read reports any\nsurvivor or newly observed transfer target without retrying. No process, registration or local\nfile is removed.\n\n## Stopped reset in 0.91.0\n\n`reset store <space>` and `reset space <space>` are the named replacements for\n`clean store` and `clean all`. `reset store --dry-run` provides a partial\nlocal-impact preview of the store path, recorded processes, maintenance journal\nand broker credless reachability. `reset store` requires confirmation,\nacquires the maintenance lock, rechecks its exact store identity, legacy\nprocesses and all recorded service instances for that space, and calls the\nexisting store-only cleanup. Any recorded supervisor blocks reset. It keeps trust and the\nregistration. TLS-required or TLS-first store targets refuse, because no trusted\nstopped-broker check covers them. `reset space` now routes a\nreviewed local identity reset through native clean-all. It requires Linux, an\nisolated locally started registration and plain-NATS loopback nonresponse.\nIts preview inventories provider-declared keys and native filesystem targets;\napply rechecks the scope under the maintenance lock and constrains each delete\nto that reviewed inventory. Trust is removed last, followed by the unchanged\neligible registration. Unknown effects are not replayed. Service definitions,\npersonas, packages, IdP logins and remote data stay. Unsupported providers,\nforeign material, TLS-first and non-Linux targets refuse.\n\n## Backup maintenance in 0.91.0\n\n`backup create` supports one-document `--json` and plain reports. The completion receipt follows\nnative attempt cleanup. A published artifact with unconfirmed cleanup is partial; retain its\npath and original attempt instead of repeating creation. `backup inspect` reads metadata and\n`backup verify` checks declared file sizes and hashes. Neither proves restoration or authority\ncontinuity, starts a broker or writes a staging tree.\n\nThe canonical stale pre-commit recovery is `backup recover <full-attempt-id>`. Healthy retained\nold-source cleanup is `backup cleanup-fallback <full-attempt-id>`. Review `--dry-run`, then use\n`--yes` in scripts. Neither flag bypasses owner, deadline, root, journal revision or store-identity\nchecks. Restore startup stays on its guarded existing route.\n\nA controller-owned stack cannot be preserved in 0.91.0. On a root with a stack journal,\n`down --preserve-state`, `backup create`, `backup restore` and `up --restore` refuse `UNSUPPORTED`\nbefore any effect. Stop such a stack with plain `cotal down`; it keeps its store, channels and\nhistory for the next `cotal up`. Preserve, backup and restore of a controller-owned stack return in\na later release. A root without a stack journal keeps its preserve, backup and restore.\n\n## Feedback input in 0.91.0\n\nUse a protected `feedback --key-file <path>` rather than placing an intake key on argv.\nThe old `--key` spelling refuses without sending or echoing its value. Public feedback still\nworks with a contact email and requires no environment setup. JSON/plain output is one public\nreceipt, not a copy of the submitted body. Unknown submission outcomes are not retried, and no\nredirect or alternate credential/intake is substituted.\n\n## Credential diagnosis in 0.91.0\n\n`doctor auth` uses JSON/plain reports and distinguishes local claims from live authorization.\nA plain diagnostic that encounters material requiring migration refuses without moving it.\nExplicit `--fix` retains the native static daemon repair, with separate migration/re-sign receipts\nand no user-auth signer fallback. A local re-sign is not daemon adoption or broker acceptance.\nUnhealthy, partial and unknown results retain their classified outcomes; scripts must read the\nnew result and error fields instead of scraping the old `auth: healthy` line. Never respawn from\na locally ordered older credential file alone.\n\n## Service adapter declarations in 0.91.0\n\nA local-process provider can expose the version-1 workstation service ABI alongside its existing\nprocess metadata. It declares a closed option schema, explicit foreground/multi-instance/log\ncapabilities and launch/readiness hooks. Re-add an extension after adding this ABI so its cached\nservice metadata and package fingerprints are recorded. An older descriptor is not imported to\nguess its schema. Plain process definitions keep their existing meaning.\n\nPresets are declarative registry contributions. Their option flags, help, completion and guided\nquestions derive from the adapter schema. Re-add a preset provider to populate its new cache;\nmissing or changed metadata refuses without importing code to guess the definition.\n\nThe top-level `feedback-intake` launcher is a [deprecated entry](#deprecated-daemon-entries). For a\nnew intake, save a definition with\n`service add <name> --preset feedback-intake --space <s> --root <absolute-root> --keys <private-keys.json> --creds <private-creds>`, then use `service start <name>` in that space. An explicit\n`--store` must be an existing private file. The default is private and instance-keyed. No broker\nor other service is started, no signer is minted and required TLS is preserved. Native startup\nreceipts are private, request-specific observations rather than native endpoint serving claims.\n\nA service instance pins its adapter name, package version and declaration fingerprint. A later\npackage update does not silently adopt a running or retained instance under a different adapter.\nStop the exact owned process and explicitly remove its stopped records before choosing changed\nadapter metadata. Logs remain private local files. Extension removal refuses live, foreign or\nunresolved service instances, including records outside the current mesh selection. None of\nthese declarations migrate legacy PID records or supply OS-unit ownership. Native manager\nimpact comes from its explicit adapter's attributed current read, not cached metadata.\n\nThe delivery implementation now registers its service adapter and legacy custody descriptor.\nCustom composition roots that want those inventory/control rows import `@cotal-ai/delivery`.\nThe full binary already composes it. CLI stop helpers share the workspace descriptor without an\nimplementation-to-implementation import. The `delivery` preset requires the recorded root and\nexisting scoped credential, keeps the native store/account/lease guards and does not adopt an\nexisting legacy daemon. Its hidden child entry is recognized by the same process-role predicate.\nThe old `deliver` machine-facing route remains for the stack callers that still use it.\n\nFresh static-auth and open `team boot` now use controller-owned stack members and accept\n`--foreground`. Their manager receives a digest-pinned manifest launch spec, with no additive\nownership ledger. A recorded stack can resume only after a completed whole-stack stop with its\nmanager never attempted, recorded broker inputs and each member's settled native evidence. Legacy\ncustody, prior team boots, supervisors and unresolved attempts still refuse. User-auth preparation\nuses ordinary `up --user-auth`, login and an explicit grant, then `down` before `team boot`. No\ncaller or owner grant is substituted.\n\nNew controller journals record the broker store path, storage cap request and bind host. A later\npreparation keeps those inputs and refuses differing explicit values. Older journals remain\nreadable. If their broker is stopped, restart requires the original store named with `--store-dir`\nand the original `--max-file-store` and `--host` values, if set. The store must already be a real\ndirectory. Do not omit the cap or host unless the original start set neither. That restart records\nthe inputs; run `down` before the stopped-stack team route. No store path is inferred from an\nabsent field, and bare restart never silently opens a default empty store.\n\nThe native manager status contract adds optional caller-scoped stop-impact metadata and advances\nthe cluster revision. Upgrade the composed manager and its compatible registration host together;\na host that does not accept the current contract must refuse registration. Older managers can\nomit the observation. Component status then says impact was not reported, rather than inferring\nthat seats survive. An observed policy does not migrate PID records, reserve a stop or confirm\ncleanup. The manager package adds a separate controller-only `manager-service` adapter and\n`manager` preset. Existing legacy manager records are not migrated or adopted. Update the\ncomposed packages and extension metadata together; an old cached declaration is not rewritten\nbehind a retained instance. Native stops require a fresh full-scope read and unchanged request-bound\npolicy, with native returned evidence plus original process exit. Missing terminal evidence retains\nits reservation and blocks replacement, without force kill. Cleanup remains not checked.\nThe direct service route currently refuses foreground mode. On an open mesh there is no\nauthenticated caller, so readiness and stop impact come from the pinned child's own request-bound\nlocal receipt. Its registration must equal the launch identity bound to that PID. Status shows it\nas a child-reported local receipt, not an attributed endpoint response. OS installation and\nnon-Linux protected process support are not available.\nRecorded systemd user units with an original invocation ID use that invocation's native journal\nfor finite/follow logs. Missing invocation evidence refuses instead of reading a stale direct-child\nfile. Native cursors and timestamps are retained; journal retention/access is not proof of complete\noutput or completed service work. No unit install/control support is implied by this read route.\n\nAuth providers may implement the optional read-only `preflightServer` hook. It checks public\nserver inputs without creating a directory, pin, key, grant or daemon. The controller's user-auth\nroute requires it before preparation; providers without it refuse rather than skipping validation.\n`prepareServer` remains authoritative and checks again before provisioning.\n\nThe auth package also declares an `auth-service` preset over its prepared native key projection.\nIt does not migrate or reclaim existing PID records. Its real child claims custody before opening\nplanes and publishes a private startup phase only after native initialization and signal handlers.\nThis route declares shared foreground control and still refuses remote broker and TLS-required\ntargets. The transport refusal is\nintentional, because not every native authority connection carries the registered transport\nrequirement. No plaintext substitution or new issuer is used.\n\nManager maintenance now supports finite JSON/plain reports. Gate repair and seat drain default\nto cancellation in a terminal and require `--yes` in scripts. Use `manager reconcile --dry-run`\nor `manager seats --drain --dry-run` first. Gate repair requires one registered root/space/broker\nand carries its TLS requirement through native liveness/eviction. It does not dial a default broker\nwhen that target is missing. An interrupted submitted repair or unresolved drain can return exit 3;\nconfirmed native results remain distinct from unconfirmed effects. No process is started.\n\n## Linear reports in 0.91.0\n\nUpdate the installed Linear extension with the matching CLI/workspace generation. Account add/show,\ninventory and direct call/resource/prompt JSON now use version-1 envelopes: read the native\ninventory from `result.inventory` and a native request reply from `result.reply`. Rich/plain reports\nbound displayed rows and lines without truncating JSON. MCP tool `isError` remains a result with\nexit 0 and a warning, not a work-success claim. Native not-executed refusals stay distinct from\nunknown submissions, and neither is replayed automatically.\n\nThe `linear` service preset runs the same native endpoint implementation from an explicit static-auth\nroot and existing account. It retains native registration, renewal and bounded deregistration; no\nnew generic caller is issued. Its private phase proves handler initialization only, not a current\nendpoint reply or completed deregistration. Foreground service start uses the shared terminal\nlifetime and native signal handlers, not the old serve parser. User-auth/open targets and\nOS-supervised service operation refuse. The older `linear serve` route remains available for\ninstalled callers.\n\nA bare Linear/account group opens no account. Wrong-mode flags or extra operands refuse before\naccount access. Interactive OAuth remains a human flow without JSON. Foreground serve stays on its native human-output path.\nStatic caller issuance keeps its native authority and now returns shared JSON/plain private-file\nreceipts. Use an existing protected physical parent outside managed state; an existing output\nrefuses before provisioning. Uncertain provisioning/publication is not replayed and no environment-\nexport launch recipe is printed. The supported `auth mint --endpoint` form resolves its scope live;\ntargeted/journaled/action issuance and consuming launch adapters are not supported. These changes do\nnot replace ownership, start a service or print secrets.\n\n## Setup inputs in 0.91.0\n\n`cotal setup` takes four inputs: destination, application, project folder and space. Nothing on a\nrunning mesh changes, and there is no outage window.\n\n### What stops working\n\n- A `cotal setup --yes` or a setup with no terminal exits 1 with `USAGE_INVALID` unless it names\n every input as a flag. It no longer sets up every detected connector and seeds the folder it\n runs in.\n- Setup sets up one application, the one `--harness` names, and records it as the space's\n `harness` preference. An application whose executable is not on PATH exits 4 with the rest\n saved; it used to be skipped with a warning.\n- The default persona lands in `<project>/.cotal/agents`. Setup no longer looks for a running mesh\n to seed into.\n- A repeat `cotal setup` no longer prints a status card. `cotal status` reports the same state.\n\n### Before the upgrade\n\nA script or image that ran `cotal setup --yes` names the four inputs:\n\n```bash\ncotal setup --destination local --harness claude --project <dir> --space <name> --no-input\n```\n\nFor a space that runs elsewhere, register it first with `cotal space add`, or pass the bundle its\noperator exported with `--connection-file`, and use `--destination self-hosted` with the\nregistration's root as `--project`.\n\nStartup remains opt-in. `--start` conflicts with `--configure-only`; a script, JSON or plain\ninvocation must add `--detach`. `--yes` by itself still saves configuration only. Startup failure\nleaves completed preparation in the result. Do not treat an unknown startup result as permission\nto submit another launch; inspect the selected space and recorded original request first.\n\n## Reads that install nothing in 0.91.0\n\nA read no longer installs or refreshes the built-in connectors and the web dashboard before it\nruns. Nothing on a running mesh changes, and there is no outage window.\n\n### What stops working\n\n- On a fresh machine, `cotal status`, `cotal ext list`, `cotal ps`, `cotal runtimes`, `cotal docs`\n and the other reads leave the extension root empty. `cotal setup`, or any command that is not a\n read, installs the built-ins as before.\n- After an upgrade, a read does not refresh the built-ins. Where it would load one still at the\n older version, such as a connector's setup row in `cotal status`, it names the package and its\n version with `cotal ext seed` instead of loading it.\n\n### Before the upgrade\n\nA script that ran a read to install the built-ins, such as `cotal ext list` right after\n`npm i -g cotal-ai`, runs `cotal ext seed` instead.\n\n## Space selection in 0.91.0\n\n`cotal use` and the target a command picks without `--space` change. Nothing on a running mesh\nchanges, and there is no outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal use <space>` no longer checks the broker. It selects any registered space, including a\n stopped one, and the command that targets it reports the space not up. A name that is not\n registered exits 2 with `TARGET_NOT_FOUND` instead of 1. Bare `cotal use` reports the selection\n instead of exiting 1 with its usage, and `--json` writes one envelope.\n- A command run without `--space` stays on the selected space when its broker is down and refuses\n with `TARGET_UNAVAILABLE`, exit 2. It used to fall back to the one other running mesh and print\n a note that the default mesh was down. A selection that names a space no longer registered now\n refuses with `TARGET_NOT_FOUND` instead of falling back to the local project or the one running\n mesh.\n- `cotal space` lists the registered spaces. `cotal meshes` still lists them, and its `--json`\n lines keep their shape.\n\n### Before the upgrade\n\nA script that relied on the fallback names the space it means with `--space`, or selects it with\n`cotal use <space>`. A script that read `cotal use` exiting 1 for a stopped space checks the\nbroker with `cotal space show <space> --json` and reads `result.check`.\n\n## Extension listing output in 0.91.0\n\n`cotal ext --json` and `cotal ext list --json` now write one JSON document instead of one JSON line\nper extension. Nothing on a running mesh changes, and there is no outage window. Upgrading the\n`cotal` binary is the whole operation.\n\n### What stops working\n\n- A script that reads `cotal ext list --json` line by line reads one envelope instead. The rows it\n read are now `result.items`, each with the same fields, and `result.root` is the install prefix.\n With `jq`, `cotal ext list --json | jq -c '.result.items[]'` prints the rows as before. An empty\n prefix writes an envelope with no items instead of nothing.\n- A usage error from `cotal ext` exits 1 as one `refused` envelope under `--json`, and `--plain` or\n `--json` on `ext add`, `ext remove`, `ext root` or `ext seed` is refused. `cotal ext root` still\n prints the path alone.\n- The human listing is a table now. Human output is not a parsing target; use `--json`.\n\n### Before the upgrade\n\nSearch scripts for `cotal ext list --json` and `cotal ext --json`, and change line-by-line reads to\nread `result.items` from the one document.\n\n## History reads in 0.91.0\n\n`cotal history [channel]` reads retained messages. Clearing is now `cotal history clear`, which asks\nin a terminal and takes `--yes` in a script.\nThis changes the CLI grammar only. Stored messages, credentials, agents and running services do not\nmigrate or restart, and there is no outage window. Upgrade the CLI and its first-party components\ntogether through the normal update path; a separate broker or manager can stay running.\n\n### What stops working\n\n- Replace a script's `cotal history --force` with `cotal history clear --yes`. `--force` is refused\n on every `history` form, `history clear` included, rather than clearing anything. Add `--dms` to\n the explicit clear command only when the purge should include direct messages and the identity\n holds that authority.\n- `history clear` accepts no channel argument, read filters or `--follow`. It purges every channel\n in the selected space; these inputs are refused rather than treated as a selective purge.\n- Bare `history` reads channels within the caller's read grant. It never clears stored messages.\n Use `--json` and `result.items` for scripts; the human report is not a parsing target.\n- A user-auth history read uses the connection's channel grant. DM audit history and stream-sequence\n cursors are not available through that ordinary human credential. Use a time with `--since`.\n\n### Before the upgrade\n\nFind scripts that pass `--force` to `history` and change their explicit purge invocation. No stored\nstate or credential backup is needed for this CLI-only transition. Keep the deployment's usual\nbackup procedure before an intentional history purge; reads do not make a backup or restore data\nthat retention has already removed.\n\n## Status output in 0.91.0\n\n`cotal status` writes one structured document with `--json`, takes `--plain` and `--all`, and exits by\nthe structured outcome table. Nothing on a running mesh changes, and there is no outage window.\nUpgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal status` exits 4 when it could not read part of what it reports, such as a PID record, the\n folder's trust material, a persona catalog, the mesh registry or one of its records, or a user-auth login. It exited\n 0. An unreachable broker, a stopped process\n or an unbound responder still exits 0.\n- The recorded meshes list checks only the selected mesh and reads `not checked` for the others. Pass\n `--all` to check every one.\n- The report no longer lists the roster or prints a `cotal status` title line, and its standalone\n hints moved to a `Hints` section at the end. Rows are labeled `name: value`. Human output is not a\n parsing target; use `--json`.\n- `--components` is refused with `--json`, `--plain` or `--all`. Its exit, `0` to `3` by component\n health, replaces the report's, so it does not exit 4 for a partial report.\n\n### Before the upgrade\n\nSearch scripts and monitors for `cotal status`. A check that treats any nonzero exit of bare\n`status` as an outage should treat 4 as a report with unreadable parts. A script that read the\nroster from `status` should use `cotal ps` for managed seats.\n\n## Read command output in 0.91.0\n\n`cotal ps --json` now writes one JSON document instead of one JSON line per seat, and `ps`, `models`\nand `runtimes` exit by the structured outcome table. Nothing on a running mesh changes, and there is\nno outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- A script that reads `cotal ps --json` line by line reads one envelope instead. The rows it read\n are now `result.items`, each still the manager's row unchanged, and `result.managers` names each\n manager instance and whether it answered. With `jq`, `cotal ps --json | jq -c '.result.items[]'`\n prints the rows as before. Instance headers no longer go to stderr under `--json`.\n- An incomplete manager census, where a registered manager did not answer, refused or serves a\n different contract, exits 4 instead of 1.\n- A mesh that `ps` or `models` cannot resolve or reach exits 2 instead of 1, and a space catalog\n refresh that `ps` or `models` needed and could not get exits 2 instead of 1. Under `--json` each is\n one `refused` envelope whose `error.code` names the reason.\n- `cotal models` exits 4 when a connector's catalog could not be read, and `cotal runtimes` exits 4\n when an installed runtime provider fails to load. Both exited 0. `cotal models --agent <name>`\n exits 5 when that one catalog could not be read. OpenCode 2.x, which has no catalog the CLI can\n list, is reported `supported: false` with the reason in `unavailable` and does not fail the read.\n- The human reports of `ps`, `models` and `runtimes` are tables now, and `ps --wide` prints one\n labeled record per seat instead of a facts line under each row. Human output is not a parsing\n target; use `--json`.\n\n### Before the upgrade\n\nSearch scripts for `cotal ps --json`, and for checks of the exit status of `ps`, `models` or\n`runtimes`. Change line-by-line reads of `ps --json` to read `result.items` from the one document.\n\n## Shell completion script in 0.91.0\n\n`cotal completion <shell>` is now `cotal completion script <shell>`, and bare `cotal completion`\nreports which shells have the stub instead of printing usage. Nothing on a running mesh changes, and\nthere is no outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\nA startup line written by hand that runs the old form, such as `source <(cotal completion bash)` or\nthe PowerShell profile line `cotal completion powershell | Out-String | Invoke-Expression`, now\nloads no completion. The command exits 1, prints nothing on stdout, so the shell sources nothing,\nand names the replacement on stderr. Replace the line with its new form:\n\n```bash\nsource <(cotal completion script bash) # bash\nsource <(cotal completion script zsh) # zsh\ncotal completion script fish | source # fish\ncotal completion script powershell | Out-String | Invoke-Expression # PowerShell $PROFILE\n```\n\nA cached stub from an earlier release keeps calling the hidden `__complete` entry. The new ownership-aware\ninstaller does not adopt it just because its text matches a generated script. Review unrecorded\nstubs and custom startup wiring manually before replacing them. Nothing automatically rewrites\na hand-edited startup file. On Linux and macOS, `completion install powershell` asks the\ninstalled shell for its current-user all-hosts profile before adding an owned block;\nit does not adopt an existing manual line. Windows profile editing still requires a\nmanual review and the printed script.\n\nNew installs record their stub hash and startup block. Reinstall and update refresh unchanged\nrecorded stubs at the same path. Update adds no startup block and does not restore a stub the user\nremoved. Uninstall removes only unchanged recorded material. Modified or unowned material is\npreserved with a diagnosis. Reopening the shell is necessary to stop using functions already\nloaded in its parent process.\n\n## CLI command resolution in 0.91.0\n\nThe CLI now resolves and validates a command before it seeds connectors or writes configuration.\nNothing on a running mesh changes. Brokers, managers, seats, credentials and mesh records need no\nmigration and no restart, and there is no outage window. Upgrading the `cotal` binary is the whole\noperation. What changes is what two kinds of command line do.\n\n### What stops working\n\n- `cotal -v` no longer prints the version. It exits 1 and names `cotal version` and `cotal -V`. A\n script that reads the version this way must use `cotal version`, `cotal --version` or `cotal -V`.\n- The words `sp`, `a`, `s`, `r`, `x` and `h` are now aliases of built-in commands (`spawn`,\n `attach`, `status`, `run`, `ext` and `history`). An installed extension that provides a command\n under one of those words still loads, but the built-in wins and every run prints a warning naming\n the extension. Remove or update that extension to clear it. `cotal ext add` refuses a new\n extension whose name or alias is already claimed.\n\n### Before the upgrade\n\nSearch scripts and shell aliases for `cotal -v`, and run `cotal ext list` to see whether an\ninstalled extension provides one of the words above. There is nothing to snapshot. The extension\nmanifest an older binary wrote is read as before.\n\n## Messaging commands in 0.91.0\n\n`cotal channels` is now `cotal channel`, and `cotal send` names its destination with a flag instead\nof a `dm`, `msg` or `ask` word. Nothing on a running mesh changes, and there is no outage window.\nUpgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal channels`, with or without `list`, `set` or `default`, exits 1 as an unknown command. Bare\n `cotal channel` lists the channels, `cotal channel set <name>` saves one channel's settings, and\n `cotal channel defaults` reads or updates the space-wide defaults.\n- `cotal send dm <agent> \"<text>\"`, `cotal send msg <channel> \"<text>\"` and\n `cotal send ask <role> \"<text>\"` exit 1 with `USAGE_INVALID`, because they name no destination\n flag. Use `cotal send -t <agent> \"<text>\"`, `cotal send -c <channel> \"<text>\"` and\n `cotal send -r <role> \"<text>\"`.\n\n### Before the upgrade\n\nSearch scripts and shell aliases for `cotal channels` and `cotal send`, and change each line to the\nnew form above.\n\n## Managed Claude workspace trust in 0.80.0\n\nBefore upgrading, snapshot the manager's private Cotal state and Claude configuration home.\nPause new managed spawns while upgrading the connector, core, manager and chosen terminal\nruntime together. For tmux, cmux, Herdr, Orca and custodial Linux PTY seats, use that runtime's\nsupported manager-reload path and observe its custody/release bounds. Default `auto` selects\nin-process `pty`, whose seats cannot survive a manager restart. Drain those seats at an approved\nidle window, or use the documented preserve-state cut and `up` recovery, which resumes them\nrather than keeping their processes running. This upgrade does not require a broker restart.\n\nManaged launches now accept the authorized workspace through Claude's native startup dialog. They\nno longer require a prior interactive visit to that directory. Claude retains its normal\nrepository and linked-worktree trust semantics, including any trust it persists for later\nsessions. Foreground launches still prompt. No tool-permission bypass flag is added, but\naccepting workspace trust enables that checkout's hooks and settings, including its default\npermission mode unless a launch flag overrides it. Authorize only reviewed workspaces.\n\nThe launch recipe includes optional startup choices before its final confirmation. A custom\nruntime must implement `LaunchSpec.confirmBefore`, or refuse a recipe carrying it. Ignoring\nit leaves an untrusted launch at the native dialog until startup times out. The new launch\nrecipe does not require restarting an existing seat; manager reload continuity remains\nruntime-dependent as described above. Adopt the new runtime before launching new seats.\nNo credentials, mesh records or operator configuration files need manual migration.\n\n## AG-UI `takeCodePoints` export in 0.79.0\n\n`@cotal-ai/connector-core` no longer exports `takeCodePoints`. The AG-UI preview splitter is its\nonly user, and the package exported it only so the emitter could shorten a `RUN_ERROR` failure\ndetail, a path that never ran and is now removed. Nothing about a running mesh changes, and no\nshipped connector or host imports it. Only code of your own that imports it from the package is\naffected.\n\n### What stops working\n\nAn import of `takeCodePoints` from `@cotal-ai/connector-core` no longer compiles, and an ES module\nthat names it fails to link with `does not provide an export named 'takeCodePoints'`.\n\n### Before the upgrade\n\nTake a string's first `n` code points with `Array.from(s).slice(0, n).join(\"\")`, which is what the\nhelper did.\n\n## SecretStore create-only write in 0.78.0\n\n`SecretStore` from `@cotal-ai/core` has a fourth method, `create(key, value)`. It stores the value\nonly while the key is absent and resolves `true`, or resolves `false` and leaves the stored value in\nplace. The auth secret kinds made once per space now write their first value through it: the callout\naccount, the issuer keys, the owner secret and a hosted auth plane's instance identity. Before, two\nfirst calls on one space each made a value, the later `put` replaced the earlier one, and the earlier\ncaller kept using a value the store no longer held. The filesystem store implements `create` with an\nexclusive create, so a CLI root needs nothing, and stored values do not change.\n\n### What stops working\n\nA store of your own without `create` no longer compiles as a `SecretStore`. Plain JavaScript gets a\n`TypeError` at the first call that makes one of those kinds, such as `prepareServer` on a new space\nor the first `startAuthService` of a hosted context.\n\n### Before the upgrade\n\nAdd `create` to your store, decided in one atomic step by the backend, such as a conditional create\nor an insert that fails on an existing key. A read followed by a write brings the race back.\n\n## Manager-service authority refusal budgets in 0.78.0\n\n`POST /manager-service-authority` now counts its refusals against the exchange face's budgets, as\n`POST /exchange` does. On the loopback face a missing or wrong capability counts in the\ninvalid-capability window, and a refused IdP token or request counts in the refused-exchange window.\nOn the public face a refusal counts in the peer's bucket. A running mesh needs nothing, and a\nrequest that succeeds is never throttled.\n\n### What stops working\n\n`ManagerServiceAuthorityCtx` from `@cotal-ai/auth` now requires `failures` and `badCaps`, so a host\nthat builds it in an object literal without them fails to compile. The handler now calls the\n`peerKey`, `throttled` and `recordFailure` of the policy it is given. After 30 refusals in a minute\non one face, a further refusal on either route answers 429 without its reason.\n\n### Before the upgrade\n\nPass the `failures` and `badCaps` arrays your `/exchange` route counts in, so both routes share one\nbudget per face. A host that serves only this route can pass new empty arrays.\n\n## Primitive scope kind in 0.78.0\n\n`PrimitiveSpec` from `@cotal-ai/lang` replaces `opensScope: boolean` with `scope: ScopeKind | null`,\nthe scope kind a primitive opens, in the way `kind` names the effect it journals. Both engines now\nread that kind from the table. Before, they cast the primitive's name to `ScopeKind`, so a scope row\nmissing from the union compiled and reached the journal as a kind its readers did not know. A\nrunning mesh and its journals need nothing.\n\n### What stops working\n\nCode that reads `opensScope` from `PRIMITIVES` or a `PrimitiveSpec` fails to compile, and plain\nJavaScript reads `undefined` there, so a check on it never holds.\n\n### Before the upgrade\n\nRead `spec.scope !== null` where you read `spec.opensScope`, or `spec.scope` when you need the kind.\n\n## Token verifier clock tolerance in 0.77.0\n\n`validateUserToken`, and the `IdpConfig` that `verifyIdpToken` and `createIdpBridge` take, from\n`@cotal-ai/auth` no longer accept `clockToleranceSec`. Both verifiers allow a fixed 5 seconds of\nclock skew on `exp`, `nbf` and `iat`, the default they already applied. Nothing in the repository\npassed the option, so a running mesh behaves as before. Only code of your own that passes it is\naffected.\n\n### What stops working\n\nA call or `IdpConfig` that passes `clockToleranceSec` in an object literal no longer compiles. Plain\nJavaScript that keeps it still runs, and the value is ignored. A `NaN` value, such as `Number()` of\nan unset environment variable, used to turn off the expiry, not-before and issued-at checks in both\nverifiers, so an expired or post-dated token was accepted. A large value widened those checks by\nthat many seconds. Both kinds of token are now refused.\n\n### Before the upgrade\n\nRemove `clockToleranceSec` from each call and each `IdpConfig`. A host whose clock is more than 5\nseconds off the issuer's needs its time synchronized.\n\n## Auth context closure in 0.71.0 (unreleased)\n\nExisting deployments need no credential migration or restart for these additive APIs. Embedded\nhosts can now inspect `handle.connections()` and await `handle.closed` after `close()` or `drain()`\nto prove every owned transport ended, including the callout, replaced readiness readers and\nshort-lived clients. The inventory is a detached snapshot.\n\nA transport close failure now rejects with its connection label. The terminal signal stays pending\nwhile any connection remains live. Repair the failure and retry `close()` before awaiting\n`handle.closed`. Closing one hosted context does not close another account's context.\n\nRead a space's claim with `readPlaneClaim(kv, space)` on that account's leader-only auth bucket.\nAn unclaimed space returns `undefined`; held and released rows retain their claim identity.\nDeleted, malformed and foreign-space rows refuse. `PlaneClaimRow` and `PLANE_CLAIM_KEY` are exported.\n\nUse `observeAccountLivenessWithCreds({ servers, observerCreds, accountId, options })` with the\naccount-scoped membership-observer credential to list that account's connections. It never widens\ncredentials or evicts connections. Zero rows prove absence only with a complete sweep and the\nsingle-server proof. An embedded endpoint's trusted composition can retain transport custody\nthrough `EndpointOptions.onConnection`.\n\n## Unreleased\n\nOn a per-user-auth mesh, a spawn-scoped caller can arm the event plane of a child under its own\nowner without `admin`. This fixes owned spawns in spaces whose registration policy requires the\nplane. Upgrade the manager to pick up the admission change. No credential or state migration is\nneeded. Cross-owner arming still requires `admin`, and the child's own-channel rule and ledger\nenvelope are unchanged. A silent non-owner caller in a space without the policy still has the\nplane disarmed, with a notice if provisioning succeeds.\n\n## Hosted auth plane identity in the store (unreleased)\n\nA hosted context started through `startAuthService` keeps its auth plane instance identity in the\ninjected `SecretStore` under `authInstanceKey(space)`, with the other auth secret kinds. It used to\nsit under `stateDir` at `.cotal/space.<hex>/auth-instance.json`, though `stateDir` holds non-secret\nstate and the record holds the plane's private serve seed. The first start of an upgraded context\nputs that record into the store, removes the file and keeps the instance. A CLI root is unchanged.\n\n### What stops working\n\nA start refuses when the store and `stateDir` hold different instance identities, and names both. A\nstore that refuses a put of the new key fails the start.\n\n### Before the upgrade\n\nLet the store accept a put of `authInstanceKey(space)`. A copy of `stateDir` taken before the upgrade\nstill holds the serve seed, so delete it or protect it as secret material.\n\n## Injected SecretStore naming in 0.76.0\n\nThe manager and the delivery daemon name an injected `SecretStore` through\n`injectedSecretStoreIdentity` from `@cotal-ai/core`. The rule is unchanged: the store's declared\nidentity, else the coordinate in `COTAL_SECRET_STORE`, else a refusal. A running mesh needs nothing.\n\n### What stops working\n\n`reloadStoreIdentityOf` from `@cotal-ai/delivery` takes `{ injected: true, store }` or\n`{ injected: false, identity }`. A call that passes `injected: true` with no store fails to compile,\nand plain JavaScript gets a `TypeError`. Both processes refuse an unnamed injected store with one\nmessage, which starts `an injected SecretStore must declare its identity`, so a log match on either\nold message no longer matches.\n\n### Before the upgrade\n\nPass the injected store to `reloadStoreIdentityOf`, or call `injectedSecretStoreIdentity(store)`.\n\n## Manager-service authority policy flag in 0.76.0\n\n`handleManagerServiceAuthority` from `@cotal-ai/auth` no longer reads `allowManagerAuthority` from\nits policy argument, and the field is removed. Both exchange faces set it to `true`, so it never\nrefused anything. A running mesh needs nothing: the public listener serves\n`POST /manager-service-authority` as it did before, and the docs now list that route.\n\n### What stops working\n\nA host that serves the route itself and passes a policy object literal with\n`allowManagerAuthority` fails to compile with TS2353. Plain JavaScript that set it to `false` got a\n403 from the handler, and its requests now reach the capability and IdP checks.\n\n### Before the upgrade\n\nRemove `allowManagerAuthority` from the policy you pass. Where you set it to `false`, leave the\nroute out of that listener's route table instead.\n\n## Hermes model from the environment in 0.68.0\n\nA connector now launches on the model and variant its launcher resolved (the `--model` or\n`--variant` flag, else the agent file's `model:` or `variant:`) and no longer reads them again from\nthe agent file. The Hermes connector also no longer takes a model from `HERMES_MODEL` in the\nenvironment of the process that spawns the seat, including when `spawn.env` lists it.\n\n### What stops working\n\nA Hermes spawn whose only model was `HERMES_MODEL` in the spawning environment is refused at launch,\nand the refusal names both ways to set a model. Spawns that set `--model` or `model:` are unchanged,\non every connector.\n\nCode that calls a connector's `buildLaunch` directly with only `configPath` now gets no model or\nvariant from that file. Pass them as `model` and `variant`.\n\n### Before the upgrade\n\nMove each Hermes seat's model from `HERMES_MODEL` onto its spawn with `--model`, or into its\npersona's `model:`.\n\n## Run answers on a participant manager in 0.68.0\n\nA participant manager now asks its issuing host for an answering credential by naming the run and\nstep it answers. The host reads the pause's token off that run's journal and no longer accepts a\ntoken from the manager. Runs on a mesh with no participant manager are unaffected.\n\n### What stops working\n\nWhile a participant manager and its issuing host run different sides of this release, the host\nrefuses every `cotal run answer` and every amendment that manager serves, because each side refuses\nthe other's request shape. Starting, resuming and reading runs is unchanged. A pause stays waiting\nthrough the window, or follows its timeout if it has one.\n\n### Before the upgrade\n\nUpgrade the auth service and every participant manager registered with it in the same window, then\nanswer the pauses that waited.\n\n## Headless OpenCode handshake in 0.69.0\n\nWith `COTAL_SERVE_HEADLESS=1`, the OpenCode launcher's `[cotal-serve]` line on stdout now carries\nonly `port` and `session`. The server password no longer appears in it, and the 1.x TUI no longer\nreceives the password on its command line.\n\n### What stops working\n\nA headless host that read `password` from that line has no password, and the server refuses its\nrequests. Seats with a TUI, and headless seats that no host drives, are unaffected.\n\n### Before the upgrade\n\nHave each headless host mint a password and pass it to the launcher as `OPENCODE_SERVER_PASSWORD`,\nthen use it for basic auth as before. Without that variable the launcher mints its own.\n\n## Filesystem store identity in 0.69.0\n\nThe delivery daemon's answer to the manager's store check now names a filesystem store by its root\nand by a random `id` that the store records once in `store.id` inside its own directory:\n`.cotal/store.id` for a workspace root, or the directory of the file for `cotal deliver --creds\n<file>`. A manager no longer counts the daemon's store as its own because the two roots have the\nsame path. On a split whose broker host and manager host use one root path, the manager host now\nstays off the daemon-credential renewal lease, so `cotal doctor auth --fix` on the broker host can\nrenew the daemon credentials.\n\n### What stops working\n\nA manager and a delivery daemon on different sides of this release refuse each other's answer to\nthe store check. The manager then remints no daemon credential, and a manager that is booting does\nnot start. This is read from the code and was not measured across two releases. A\n`cotal deliver --creds <file>` whose directory is a read-only mount and holds no `store.id` stops at\nstart. So does a `--creds` file that is its directory's `store.id` under any name, and a `store.id`\nthat is a symbolic link or holds anything but a lowercase UUID.\n\n### Before the upgrade\n\nUpgrade the broker host and every manager host of a space in the same window. For a `--creds` file\non a read-only mount, add a regular `store.id` file beside it that holds a new lowercase UUID and no newline,\nas `node -e 'process.stdout.write(crypto.randomUUID())' > store.id` writes. Move a `--creds` file\nnamed or linked as `store.id` to a file of its own.\n\n## Detached spawns with `--share-tools` in 0.69.0\n\nThe manager's `spawn` operation now takes `shareTools` as a list of MCP server names. The CLI parses\n`--share-tools` into that list before it sends the request, and the manager cluster document moves\nto revision 22. A cut taken with `cotal down --preserve-state` before the upgrade still resumes: the\nmanager reads its `cotal-manager-resume/v1` inventory and writes new cuts as\n`cotal-manager-resume/v2`.\n\n### What stops working\n\nA CLI and a manager on different sides of this release refuse a detached spawn that passes\n`--share-tools`, because the CLI checks each request against the contract the manager serves. This\nis read from the code and was not measured across two releases. A detached spawn without the flag,\na foreground spawn and a roster entry are unaffected. A manager older than this release cannot\nresume a cut that this release took.\n\n### Before the upgrade\n\nUpgrade the CLI on every host that runs `cotal spawn --detach` in the same window as the managers\nit reaches.\n\n## Shared MCP server checks in 0.69.0\n\nThe cotal config reader now checks each server under `connectors.<name>.mcpServers` when it reads\nthe file, and refuses one that cannot launch as written, naming the file and the field. The rules\nare in [the config file](config.md#the-config-file).\n\n### What stops working\n\nA config file that holds such a server refuses every Claude spawn that reads it, including one with\n`--share-tools none`. Before, a field of the wrong type failed each Claude spawn that shared the\nserver with a `TypeError` that named neither the file nor the server, a spawn that did not share it\nlaunched, and a server with no `command` or `url` was passed to `claude`, which never started it.\nRead from the code and not measured: spawns on other connectors, a manager resume and the step of\n`cotal setup` that records the shared list read the same files, so each stops at the same refusal.\n\n### Before the upgrade\n\nCheck `connectors.<name>.mcpServers` in the operator-level config file and in each space's\n`.cotal/config.json`. Give each server a string `command`, or a `type` of `http`, `sse` or `ws` with\na string `url`. Write `args` as a list of strings and `env` and `headers` as objects of strings, or\nremove the server.\n\n## Remote manager family eviction in 0.69.0\n\nA remote manager registered through its host now asks the host to evict up to 256 holders of its\ncredential family in one maintenance request, and the host reads the family once for the whole set.\nBefore, a restart sent one request per holder and the host read the whole family for each one.\nMeshes with no remote manager are unaffected.\n\n### What stops working\n\nWhile a remote manager and its issuing host run different sides of this release, each side refuses\nthe other's eviction request shape. A restart whose credential family already has holders then fails\nat its eviction step and leaves the manager's registration gate frozen. A first start, a clean stop\nand the host's reconciliation of a foreign slot holder are unchanged.\n\n### Before the upgrade\n\nUpgrade the auth service and every remote manager registered with it in the same window. A manager\nthat restarted inside the window resumes its frozen registration on its next start once both sides\nrun this release.\n\n## AG-UI emitter holder hooks in 0.69.0\n\n`AguiEmitterHolder` from `@cotal-ai/connector-core` now takes its hooks as one named object after\nthe emitter factory: `new AguiEmitterHolder(startEmitter, { onError, onRunClosed, waitLive, runMeta })`.\nOnly `onError` is required. Nothing about a running mesh changes, and every shipped connector passes\nits hooks by name. Only a connector of your own that builds a holder is affected.\n\n### What stops working\n\nA holder built with positional hooks, such as `new AguiEmitterHolder(start, onError, onRunClosed)`,\nno longer compiles, because the constructor takes two arguments. Plain JavaScript that keeps the\npositional form still runs, but the holder calls none of its hooks, so a failure never reaches\n`onError`.\n\n### Before the upgrade\n\nPass each hook by name, for example `new AguiEmitterHolder(start, { onError, onRunClosed })`, and\ndrop any `undefined` that filled an earlier slot to reach a later hook.\n\n## Worker run failure type in 0.70.0\n\n`WorkerRunFailed`, the failed result of `runInWorker` in `@cotal-ai/lang`, is now a union on\n`class`: `released`, `held`, `effect`, `too-large`, `rejected` or `error`. A running mesh needs\nnothing, because the runtime host and the engine thread ship in the same install. A run on the\ncompiled engine whose program throws an object with `code: \"L5012\"` or `code: \"L5025\"` used to end\nreleased and now ends failed, as it does on the walker.\n\n### What stops working\n\nTypeScript code that reads `code`, `reason`, `step`, `pending`, `kind`, `detail` or `tooLarge` on a\n`WorkerRunFailed` it has not narrowed fails with TS2339. A released, held, too-large or rejected\nresult no longer carries `code`, so JavaScript that branched on `L5012`, `L5025`, `L5006` or\n`L5010` stops matching with no error. `tooLarge` is gone.\n\n### Before the upgrade\n\nBranch on `class` where such code read `code`: `released` for L5012, `held` for L5025, `too-large`\nfor L5006 and `rejected` for L5010. An `effect` or `error` result keeps its `code`. Once narrowed to\n`too-large`, a result carries the `stepKey`, `bytes` and `bound` that `tooLarge` held.\n\n## Remote manager request builder in 0.70.0\n\n`remoteManagerClient.remoteManagerAuthorityRequest` from `@cotal-ai/manager` now takes an\noperation's coordinates as one object, and `remoteManagerRegistrationProof` from `@cotal-ai/core`\ncomputes the proof from the manager's identity state instead of a request. Nothing about a running\nmesh changes: the proof digest and the request on the wire are the same, so a manager and a host on\ndifferent sides of this release still accept each other. Only code that builds remote manager\nrequests itself is affected, in TypeScript and in plain JavaScript.\n\n### What stops working\n\nA call that passes the registration proof, contract artifacts, session, retirement or transfer\nreader as positional arguments after the operation no longer compiles. A call that passes a request\nto `remoteManagerRegistrationProof` no longer compiles either, because the second argument now names\nthe lifecycle `lifecycleUid`, as the identity state does.\n\nPlain JavaScript runs both old calls without an error. The builder drops the positional coordinates,\nso the host refuses the request with `requires a sha256 registrationProof`. A proof computed from a\nrequest leaves out the lifecycle, so the host refuses a request that carries it as a proof mismatch.\n\n### Before the upgrade\n\nName the coordinates, for example\n`remoteManagerAuthorityRequest(state, \"cli\", \"retire\", { registrationProof, retirement })`.\nCompute the proof as `remoteManagerRegistrationProof(owner, state)`, adding the contract artifacts\nas a third argument for activation only. A host that recomputes the proof from a received request\npasses `{ space, instanceId, lifecycleUid: managerLifecycleUid, identities }` from that request.\n\n## Bearer validator lifetime cap in 0.70.0\n\n`validateUserToken` from `@cotal-ai/auth` no longer takes `maxTtlSec`. It caps a bearer's lifetime\nat the cap of the bearer's view, the same cap the issuer applies when it mints: 900 seconds, or 300\nfor a `transfer-writer` bearer. The auth callout never passed the option, so a running mesh behaves\nas before. Only code of your own that calls the validator with `maxTtlSec` is affected.\n\n### What stops working\n\nA call that passes `maxTtlSec` in an object literal no longer compiles. Plain JavaScript that keeps\nit still runs, and the value is ignored. A `NaN` value, such as `Number()` of an unset environment\nvariable, used to turn the lifetime check off and accept a bearer of any lifetime. That bearer is\nnow refused at its view's cap.\n\n### Before the upgrade\n\nRemove `maxTtlSec` from each call. A test that needs a bearer to expire sooner mints one with a\nshorter lifetime.\n\n## Persisted identity records in 0.70.0\n\nThe manager instance identity, the manager sibling identities, the auth plane instance identity and\na participant manager's remote authority state now share one reader and one first mint in\n`@cotal-ai/workspace`, exported as `claimIdentityRecord` with the nkey check `identityOf`. Each\nrecord is read as a regular file, must hold non-empty nkeys and is created exclusively, so\nconcurrent first starts of a participant manager on one root now settle on one identity where each\nused to keep its own. `saveManagerInstanceIdentity` and `saveAuthInstanceIdentity` are gone. A\nrunning mesh whose records are plain files needs nothing.\n\n### What stops working\n\nA manager instance, auth instance or remote authority record that is a symlink, a directory or any\nother non-regular entry is refused where it used to be followed. The manager, the auth plane and a\nparticipant manager fail to start on it, and `cotal reconcile-gate` and `cotal deregister-instance`\nrefuse it. Retirement already refused it. A remote authority record with an empty nkey id or seed\nis refused too. A first mint that loses its race and cannot read the winner now refuses with\n`identity-record-create-lost` in place of `manager-instance-identity-create-lost` or\n`auth-instance-identity-create-lost`. Code that imports either `save` function no longer compiles.\n\n### Before the upgrade\n\nReplace a symlinked identity record with a copy of the file it points to. Code that wrote a record\nwith a `save` function plants it with `createManagerInstanceIdentity` or\n`createAuthInstanceIdentity`, which create the record when it is absent and otherwise return the\nstored one unchanged. Nothing replaces an overwrite of a stored identity.\n\n## Manager instance in user credentials in 0.70.0\n\n`AuthProvider.userCredentials` from `@cotal-ai/core` no longer returns `managerInstanceId`. A\n`manager-caller` credential's manager instance is the signed `act.managerInstanceId` claim in its\nbearer, which the broker verifies and the CLI already used. The reference provider in\n`@cotal-ai/auth` stops copying the exchange response's field into its result, where nothing\ncompared it with the bearer. The exchange still answers with the field, so a running mesh behaves as\nbefore.\n\n### What stops working\n\nCode of your own that reads `managerInstanceId` from a `userCredentials` result no longer compiles,\nand plain JavaScript reads `undefined` there.\n\n### Before the upgrade\n\nRead the instance from the bearer's `act.managerInstanceId` claim.\n\n## Auth plane identity location in 0.70.0\n\nThe user-auth service keeps its instance identity in the root's `.cotal/space.<hex>/auth-instance.json`,\nbeside the manager's. It used to sit inside `.cotal/auth`, at\n`space.<hex>/.cotal/auth/auth-instance.<hex>.json`, so a copy of that folder carried it. The first\nstart of an upgraded root moves the record and keeps the instance. A hosted context started through\n`startAuthService` has its record moved the same way inside its `stateDir`.\n\n### What stops working\n\nCode that calls `openAuthAuthorityPlane` without the new `identityRoot` option no longer compiles. A\nstart that finds a record both in `.cotal/space.<hex>/` and at its older place refuses and names the\ntwo files. A start also refuses when the older place of the auth or manager identity holds a symlink,\na directory or anything else that is not a regular file. The manager used to skip a dangling symlink\nthere and mint a new identity.\n\n### Before the upgrade\n\nPass `identityRoot` to `openAuthAuthorityPlane`. When `dir` is a workspace root's user-auth state\ndir, `<root>/.cotal/auth/space.<hex>`, pass that root. A plane with no workspace root, as\n`startAuthService` runs, passes `dir` itself. Either keeps the identity the plane already has: on the\nfirst start it moves from `<dir>/.cotal/auth/` to `<identityRoot>/.cotal/space.<hex>/`. Never pass a\ndirectory inside `.cotal/auth`: the record would land in the folder an operator copies and travel\nwith it again.\n\nA copy of `.cotal/auth` taken from a root last run by an older Cotal carries that root's record.\nDelete `.cotal/auth/space.<hex>/.cotal/auth/auth-instance.<hex>.json` from the root you copied it to\nbefore the first `cotal up --user-auth` there.\n\n## Per-seat `COTAL_` names in `spawn.env` in 0.71.0\n\n`spawn.env` in the cotal config no longer forwards a `COTAL_` name the launcher sets for each seat,\nsuch as `COTAL_ROLE`, `COTAL_MODEL` or `COTAL_SUBSCRIBE`. Before, a seat launched with no value of\nits own took the spawning process's value and ran under that role, model or read set. The\nmachine-wide knobs a seat already receives, such as `COTAL_HOME`, may still be listed.\n\n### What stops working\n\nEvery spawn and resume under a config whose `spawn.env` lists such a name is refused before\nlaunch, and the refusal names the entry. Code that calls `launchEnv` from `@cotal-ai/connector-core`\nwith such a name in `envAllow` gets the same error.\n\n### Before the upgrade\n\nRemove those names from `spawn.env`. Give each seat its role, model and channels with `--role`,\n`--model` and `--subscribe`, or in its persona's `role:`, `model:` and `subscribe:`.\n\n## Role addresses in 0.71.0\n\nA role must be one `[A-Za-z0-9_-]` token. Before 0.71.0 any other spelling was rewritten into one:\n` probe ` reached the `probe` queue and `pro.be` reached `pro_be`, while the message kept the\nspelling sent. An anycast to `*` was accepted and stored where no holder reads it.\n\n### What stops working\n\nAn agent whose role is outside the token set no longer starts, however it is launched:\n`cotal join --role`, `cotal spawn --role`, an agent file's `role:`, `COTAL_ROLE` and an embedded\nendpoint's `card.role` are all refused before the agent joins.\n\nA send to such a role, or to `*`, through `cotal send ask`, `/anycast` or `cotal_anycast` is refused,\nand nothing is stored.\n\n`routeToken` is no longer exported from `@cotal-ai/core`. A role routes as spelled, so code that\nused it to name a role's queue uses the role itself, and `assertValidRole` checks one.\n\n### What migrates on its own\n\nEvery task queue. A `svc_<role>` durable was always named from the rewritten token, so its pending\nrequests and its holders carry over.\n\n### Before the upgrade\n\nRename each role outside the token set to the token it already routed to: remove the surrounding\nspaces and replace every other character outside the set with `_`. Rename it where the holder is\nlaunched and in every script or prompt that sends to it.\n\n## IdP URLs on `localhost` in 0.73.0\n\nThe IdP URL that `cotal login --idp` and `cotal up --user-auth --idp` take, the JWKS URL the auth\nservice pins from it, the space-catalog link and `cotal sync` now share one rule: `https://`, or\n`http://` on a loopback IP literal. `localhost` is a name, so it no longer counts: a hosts entry\nwould choose the IdP, and with it the keys the callout trusts. Every loopback literal now passes, so\n`http://127.0.0.2/api/auth` and `http://[::ffff:127.0.0.1]/api/auth` are accepted where they were\nrefused before. A JWKS URL with any scheme other than `https:` or `http:` is refused. A mesh whose\nIdP uses HTTPS is unaffected.\n\n### What stops working\n\nOn a mesh whose IdP is pinned at `http://localhost:<port>/...`, the auth service builds its key\nresolver from the pinned JWKS URL at start, and that resolver now refuses it with\n`JWKS origin must be https (or http on a loopback IP literal for dev)`.\n\n`cotal login` and `cotal logout` with `--idp http://localhost:<port>/...` are refused with\n`idp url must be https (or http on a loopback IP literal such as 127.0.0.1 for local dev)`, and so is\nevery command that reads a session cached under that URL.\n\n### Before the upgrade\n\nUse `127.0.0.1` (or `::1`) in place of `localhost`. In the space's `idp.json` under the mesh's\n`.cotal/auth`, change `url` and `jwksUri` to the literal spelling and leave `issuer` and `audience`\nas they are. Owners derive from the issuer, so existing grants keep matching. Change a manifest's\n`broker.idp` the same way, because `cotal up` refuses an `--idp` that differs from the pin. Then\nhave each person run `cotal login --idp http://127.0.0.1:<port>/api/auth` again, because sessions\nare cached under the URL.\n\n## Terminal layout `Pane.confirm` in 0.73.0\n\n`Pane.confirm` is removed from `@cotal-ai/core`, and the tmux and cmux terminal-layout providers no\nlonger press Enter in the panes they open. The flag carried no prompt text, so both providers\npressed Enter five times, one second apart: the first dialog a pane showed was answered with its\ndefault, and a prompt that never appeared went unreported. Nothing in Cotal sets it, so a running\nmesh needs nothing. Spawned agents are unaffected, because their runtimes match the startup prompt\nby its text.\n\n### What stops working\n\nA `Pane` object literal that sets `confirm` fails to compile with TS2353. Plain JavaScript that sets\nit runs without an error, and the pane stays at its prompt.\n\n### Before the upgrade\n\nRemove `confirm` from each `Pane`. Start a command that shows a startup prompt with the option that\nskips it, or answer the prompt in the pane.\n\n## Carrying a resumed Claude session to another host in 0.67.0\n\n`cotal spawn --resume <id> --detach --on <instance>` now carries a Claude session held on the\noperator's host to the target manager instance. Both sides need this release: an older manager does\nnot serve `transcript-receive`, and the CLI then stops with that manager's refusal instead of\nlaunching. The manager cluster document moves to revision 21, and the `ps` row's `resume` object\ngains `host` and `transferredAt`.\n\nA manager host that runs carried seats needs `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_AUTH_TOKEN` or a\ncloud provider selection in its environment, because each carried seat runs in its own Claude home\nwith no stored login. On an authenticated mesh the CLI mints the transfer writer from the space's\nsigning seed, so the carrying host needs that seed, as for any other operator command. On a user-auth\nmesh it exchanges the operator's login for a `transfer-writer` view instead, so the operator's grant\nneeds scope `admin`, and the auth service must run this release. A remote manager receives a carry once\nits host serves the manager-service `transferReader` operation. A seat launched without carrying,\nincluding any `--resume` whose id this host does not hold, is unchanged.\n\n## Lifecycle head type in 0.67.0\n\n`LifecycleMapping`, the type `parseLifecycleHead` returns, is now a union on `state`. Nothing about\na running mesh changes: heads that parsed before parse the same way, and the refusals are\nunchanged. Only TypeScript code that compiles against `@cotal-ai/core` is affected.\n\n### What stops working\n\nAn `interface` that extends `LifecycleMapping` fails with TS2312, because an interface cannot extend\na union. Code that builds a head in memory no longer compiles when the head is `retiring` without\nits `op`, or `active` or `retired` with one. The parser already refused those heads.\n\n### Before the upgrade\n\nDeclare such an interface as an intersection instead, for example\n`type ActiveMapping = LifecycleMapping & { state: \"active\" }`. A reader that has checked\n`state === \"retiring\"` reads `op` without a guard.\n\n## Issuance gate types in 0.67.0\n\n`EpGateRow` and `EndpointGateRow`, which `parseIssuanceGate` and `parseEndpointGate` return, and\n`EpGateState`, which an `EpIssuanceGate` or `EpIssuanceBarrier` returns from `observe`, are now\nunions on `state`. Nothing about a running mesh changes: gates that parsed before parse the same\nway, and the refusals are unchanged. Only TypeScript code that compiles against `@cotal-ai/core`\nis affected.\n\n### What stops working\n\nAn `interface` that extends one of these types fails with TS2312, because an interface cannot\nextend a union. Code that builds a gate in memory, such as a custom barrier's `observe`, no longer\ncompiles when the gate is `frozen` or `retired` without its `op`, or `open` with one. The gate\nparsers already refused those rows.\n\n### Before the upgrade\n\nDeclare such an interface as an intersection instead, for example\n`type CustomGateRow = EpGateRow & { custom: string }`.\n\n## Lifecycle-blocked refusals in 0.66.0\n\nA refusal that carries `ai.cotal.ep.lifecycle-blocked` now reports only the lifecycle state it\nread. Nothing about a running mesh changes. A client that branches on the detail must read the new\nfield.\n\n### What stops working\n\nA refusal raised at the issuance gate used to carry `headState` without reading the head:\n`retiring` for a frozen gate and `retired` for a retired one. It now carries `gateState`\n(`frozen` or `retired`) and no `headState`. A client that treats `headState: \"retired\"` as a\nburned uid, or `headState: \"retiring\"` as a retirement in flight, no longer matches those\nrefusals, and the `[lifecycle ...]` suffix on the error string changes the same way. A custom\nissuance barrier whose `observe` returns a frozen gate without a valid `op` (a string `opId` and\none of the four op kinds) is now refused as `internal` by `registerServiceInstance`.\n\n### Before the upgrade\n\nUpdate such a client to read `gateState` for a gate refusal and `blockedOp` for the operation that\nholds the gate. `headState` is present only when the refusal read the head, for example an\nactivation refused because the head is still retiring.\n\n## Workflow programs that bind `once` in 0.65.0\n\n`once` is now a scope of the workflow language, so it is a reserved name. A program that declares\nits own `once` binding (`const once = ...`, a parameter or a function named `once`) is refused at\nvalidation with L2002. Nothing else about a running mesh changes.\n\n### What stops working\n\nA run whose recorded program binds `once` cannot be resumed after the upgrade, because a resume\nvalidates the recorded program again. A new `cotal run start` of such a program is refused before\nanything is recorded.\n\n### Before the upgrade\n\nList the runs with `cotal run ps` and check each program that is still running or held for a\nbinding named `once`. Let those runs finish on the old version before you upgrade the manager, and\nrename the binding in the program before you start it again.\n\n## From 0.58.0 to 0.59.0\n\nEvery connector now publishes a failed run's `RUN_ERROR` on `events.<owner>.<actor>` with the fixed\nmessage `run failed` and no `code` or `rawEvent`. The error text and error kind a harness reports\ncan echo a prompt, a peer message or tool output, and that channel has a different read ACL. A\nreader that showed the message or branched on `code` gets neither after the upgrade. Where a\nconnector reports the error kind as the agent's presence condition, that is unchanged.\n\n### Settle pending event frames before the upgrade\n\nEach session's events are frozen in its event write-ahead log before they are published. A session\nrestarted on 0.59.0 whose log still holds an unacknowledged frame with an older `RUN_ERROR` does not\nrepublish it: its event emitter halts with `egress-run-error` and publishes nothing further for that\nsession. The broker may or may not already hold that frame, so the halt cannot settle it.\n\n1. Stop the seats cleanly on 0.58.0, with the broker still up.\n2. List the logs that still hold a pending frame. The logs live under the events state root\n (`COTAL_WORKSPACE_ROOT`). Empty output means there is nothing to settle.\n\n ```sh\n find \"$COTAL_WORKSPACE_ROOT/.cotal/events\" -name wal.json \\\n -exec jq -r 'select(.pending != null) | input_filename' {} +\n ```\n\n3. For each session listed, start it again on 0.58.0 while the broker is reachable, let it recover,\n stop it, and run step 2 again. Recovery publishes the frame as 0.58.0 would have, error text\n included, so it only finishes what 0.58.0 had already started.\n\n If that start halts with `cas-loss` instead, the agent's subject is no longer at the sequence this\n log expects, and no restart settles that log, on 0.58.0 or later. A lost acknowledgement is one\n cause: the broker stored the frame, so it and its error text are already on the channel, and every\n retry halts the same way because the stream checks the frozen expectation before it deduplicates.\n The halt message names the other causes, such as a second emitter for the same agent under a\n different state root, a restored stream or frontier record, or a purged channel. With those the\n pending frame may never have reached the broker, so a `cas-loss` does not tell you whether it\n landed. Find and stop any second writer and rule out a restored state first. Clearing the halt\n then means purging the agent's event channel and removing the agent's directory under the events\n state root whole (see [Event plane](connect-claude.md#event-plane)). That abandons the pending\n frame whether or not the broker has it, and the purge also drops the earlier frames of every\n session of that agent.\n4. Upgrade once step 2 prints nothing.\n\nIf a session halts with `egress-run-error` after the upgrade, go back to step 3 for that session on\n0.58.0. Do not edit or delete `wal.json` on its own to get past either halt: clearing the pending\nframe abandons that epoch, an event the broker never received is lost, and removing part of the\ndirectory leaves a state the next start refuses.\n\n## Explicit actor grants in 0.59.0\n\n`cotal actor grant` no longer fills an omitted ACL flag with its wide default. A grant names\n`--scope`, `--allow-subscribe` and `--allow-publish`, or passes `--full` to give the ones it leaves\noff their wide defaults (`spawn,role:default`, `>` read, `>` post). Any other grant is refused. The\nbreak is in the CLI on the machine that holds the actor ledger, the one that ran\n`cotal up --user-auth --idp <url>`. No stored row, credential or wire message changes.\n\n### What keeps working\n\nExisting actor ledger rows keep the authority they were granted, and their users and agents connect\nas before. `actor revoke`, `actor list` and a `grant` that names all three ACL flags behave as they\ndid on 0.58.0. Nothing on disk is converted.\n\n### What stops working\n\nA grant that leaves off any of the three flags without `--full` exits 1 with\n`refusing to grant \"<actor>\" with --scope, --allow-subscribe, --allow-publish left off`, naming the\nflags it is missing, and then prints both accepted forms. It writes no row and does not retire the\nactor's current lifecycle. An existing row stays as it was, and an actor granted for the first time\nstays out until the grant is run again. This includes the bare grant printed on 0.58.0 by\n`cotal login`, `cotal status`, `actor list` and the not-granted refusal. Look for it in provisioning\nscripts, onboarding runbooks and anything that pastes those hints.\n\n### Upgrade order\n\nChange the scripts before the ledger machine is upgraded, and make each grant name all three flags.\n0.58.0 and 0.59.0 both accept that form. To keep a wide row, write its defaults out:\n\n```sh\ncotal actor grant <actor> --sub <IdP subject> \\\n --scope spawn,role:default --allow-subscribe '>' --allow-publish '>'\n```\n\nSwitch to `--full` only once the ledger machine runs 0.59.0. 0.58.0 refuses it with\n`Unknown option '--full'` before it reads the ledger. Brokers, managers and participant machines\nneed nothing for this break, so their order is the one the section above gives.\n\n### The window\n\nThis break has no outage. No process restarts for it, and a refused grant changes nothing. The\nexposure is a grant script that runs against 0.59.0 before it was changed: it fails and grants\nnothing.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. On the ledger machine, save the output\nof `cotal actor list` to compare rows after the changed scripts run, and list the scripts that call\n`cotal actor grant`.\n\n### The upgrade end to end\n\n```sh\n# on the ledger machine, still on 0.58.0\ncotal actor list > actors-before.txt\ngrep -rn 'actor grant' <your provisioning scripts>\n# make every grant name --scope, --allow-subscribe and --allow-publish, run them, then upgrade\nnpm i -g cotal-ai@0.59.0\ncotal actor list | diff actors-before.txt -\n```\n\nBoth refusals quoted here were run on 0.58.0 and on the 0.59.0 code. That brokers, managers and stored\nrows need nothing is read from the change, which touches only the CLI and its hints, and was not run\non a live split deployment.\n\n## Repeated flags refused in 0.59.0\n\nA `cotal` flag given more than once is now a usage error unless the command declares it repeatable.\nOn 0.58.0 the last value won with no message, so `cotal down web --space a --space b` acted on `b`\nwhile a wrapper that checked the first `--space` verified `a`. The break is in the command-line\nparser on the machine that runs the command, including commands added with `cotal ext add`. No\nstored state, credential or wire message changes.\n\n### What keeps working\n\nA command line that gives each flag once parses as it did on 0.58.0, in any order and in the\n`--flag=value` form. Flags whose help says repeatable, such as `--opt` and `down --session-store`,\nstill collect every value. A flag-shaped word after `--` is still a positional. The daemons, units\nand agents that `cotal` starts for itself are given each flag once, so a fleet driven only by `cotal`\ncommands typed by hand needs no action.\n\n### What stops working\n\nA command line that repeats any other flag exits 1 before the command runs. It prints\n`Option '--space' cannot be repeated`, or `Option '-f, --file' cannot be repeated` for a flag with a\nshort form, followed by the command's help. `-f` and `--file` count as the same flag. Look for it in\nscripts, aliases and wrappers that append a flag to override one set earlier, such as a fixed\n`--space` followed by `\"$@\"`.\n\n### Upgrade order\n\nChange those scripts first so each flag is given once. 0.58.0 and 0.59.0 both accept that form.\nBrokers, managers and participant machines need nothing for this break, and each machine's CLI\napplies it when that machine is upgraded, so their order is the one the sections above give.\n\n### The window\n\nThis break has no outage. No process restarts for it, and a refused command does nothing. The\nexposure is a script that still repeats a flag when it runs on 0.59.0: it exits 1 instead of acting on\nthe last value.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. List the scripts, aliases and wrappers\nthat call `cotal` so each one can be checked.\n\n### The upgrade end to end\n\n```sh\n# still on 0.58.0\ngrep -rn 'cotal ' <your scripts and wrappers>\n# give each non-repeatable flag once, then upgrade\nnpm i -g cotal-ai@0.59.0\n# run each changed script; a repeat left behind exits 1 with the usage error and does nothing\n```\n\nThe refusal and its messages were run against the 0.59.0 parser and `cotal topology view`. That the\nargument lists `cotal` builds for its own processes give each flag once is read from the code, and\nwas not run on a live split deployment.\n\n## Detached spawns from a seat's shell in 0.62.0\n\nOn a static or open mesh, `cotal spawn --detach` run inside a managed seat's shell now launches as\nthat seat. On 0.61.0 it minted a one-shot operator instrument, so the manager recorded that\ninstrument as the spawner and the seat's own `cotal_despawn` of the child was refused with\n`not authorized: <seat> was not spawned by <caller> (admin tier required)`. The break is in the CLI\non the machine where the seats run. No stored state, credential or wire message changes.\n\n### What keeps working\n\n`cotal spawn --detach` from an operator terminal or from a script outside any seat launches as\nbefore, and so does any call with `--creds`, one aimed at a space other than the seat's own, or a raw\nopen target named with `--server` and an unregistered `--space`. A user-auth mesh is unchanged. A seat with\n`capabilities: [spawn]` still spawns from its shell, and can now stop that child with\n`cotal_despawn`. `--on <instance>` from a seat's shell still lands on that manager instance, now as\nthe seat.\n\n### What stops working\n\n- On a static mesh, a seat without `capabilities: [spawn]` can no longer spawn from its shell. Its\n own credential holds no spawn subject, so the broker refuses the request and the command exits 1.\n- A child launched from a seat's shell is now that seat's child, so the manager stops it when the\n seat exits, as it does for a `cotal_spawn` child. A child that has to outlive the seat that\n started it now goes with the seat.\n- A seat launched without `COTAL_SPACE` is placed by its static credential. Every connector sets\n that variable, so this only reaches a hand-built launch: from such a seat's shell, a spawn aimed at\n a static space that holds no credential for the seat is refused instead of running as the operator.\n\n### Upgrade order\n\nOnly the CLI that seats run from their shell changes, which is the one installed on the host where\nthe seats run. Brokers and managers need nothing for this break, so their order is the one the\nsections above give.\n\n### The window\n\nThis break has no outage. No process restarts for it. A child already running when you upgrade\nkeeps the spawner the manager recorded at its launch.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. List the agent files whose seats run\n`cotal spawn --detach` from their shell, note which of them lack `capabilities: [spawn]`, and note\nwhich of their children must outlive the seat.\n\n### The upgrade end to end\n\n```sh\n# still on 0.61.0: find the seats that spawn from their shell\ngrep -rln 'cotal spawn' .cotal/agents\n# add `capabilities: [spawn]` to each of those agent files that lacks it, and launch any child\n# that must outlive its seat from an operator terminal instead\nnpm i -g cotal-ai@0.62.0\n```\n\nThe attribution, the despawn, the refusal of a seat without `spawn`, the stop on seat exit and a\nseat's `--on` spawn were run on a local static mesh, and the attribution and the despawn on a local\nopen mesh.\n\n## From 0.53.0 to 0.54.0\n\nManager calls now borrow an instance-bound `manager-caller` credential. Followed mutations require\n`manager.goal-result` on the selected manager, so a compatible issuer, manager and client must be\nloaded together. An older manager is refused before a followed mutation; upgrading an installed\nbinary alone does not replace code in a running manager, connector or embedded client.\n\n### Preserve state before changing processes\n\nSnapshot the broker's durable storage using its supported backup procedure, the host authority and\nactor ledgers, and each participant's manager identity, runtime custody records, credentials and\nsaved sessions. Include the embedding application's database and configuration under its supported\nbackup procedure. Record the loaded package versions and the CLI path used by bearer helpers.\nKeep these copies private. Do not change the IdP issuer, regenerate manager identities, rotate agent\ncredentials or recreate tenant storage to make the upgrade pass.\n\nNo ledger, goal-history or session conversion is required for this change. Existing ordinary\nmessaging credentials retain their normal expiry rules. New manager-caller credentials are obtained\non demand from the current grant; old manager-call credentials do not gain the new view automatically.\nExisting accepted goals remain durable and must not be submitted again merely because observation\nwas interrupted. Fresh remote registration publishes its service status at the current revision and\nepoch; do not seed that status manually.\n\n### Upgrade the split deployment\n\n1. Stage one pinned 0.54.0 package set for the host and participants, including the embedding SDKs.\n Pause new manager mutations and let accepted work settle where possible before reloading processes.\n2. Upgrade the host issuer and embedding first. Keep the broker, its account identities and durable\n storage in place. Then load the matching manager release on each participating machine.\n3. Preserve active seats through the runtime's supported update path. A Linux custodial runtime may\n release and re-adopt seats within its 600-second unattended window; verify the actual runtime,\n custody records and process identities before relying on it. A legacy PTY runtime without release\n support cannot preserve active seats through a generic manager restart. Drain it at an approved\n idle window instead of signalling the manager or replacing conversations.\n4. Reload the clients and connectors through their session-preserving host controls. Refresh any\n bearer helper captured from an older immutable CLI path. A transport-only reconnect does not reload\n JavaScript. Verify authenticated instance selection, a read-only manager command and canonical\n result recovery before allowing new followed mutations.\n\nTreat the interval from issuer reload through compatible manager/client reload as a manager-control\noutage. Mixed versions can refuse discovery or commands; there is no promised rolling transition.\nOrdinary agent sessions survive only where their runtime and credentials permit it. If verification\nfails, keep mutations paused and repair forward from the preserved state rather than resetting it.\nThis release does not add host-backed enrollment or terminal release for stock participant detached\nagents; see [Remote supervised agents](run-a-mesh.md#remote-supervised-agents).\n\n## From 0.48.2 to 0.49.0\n\n0.49.0 changes how a credential's authority is recorded. A credential is no longer only a signed\nfile: it is an *issuance*, with a generation the issuer chose and durable evidence of the ceiling it\nwas granted under. The important consequence for a running deployment is not at connect time. It is\nat renewal time.\n\n### What keeps working without any action\n\n- **Existing agent credentials keep authenticating.** A credential minted under 0.48.2 is not\n revoked and is not rejected at connect. Nothing needs to be re-issued to bring the fleet back up\n after the upgrade.\n- **The channel registry survives.** Channels, their replay settings, descriptions, and usage text\n are ordinary durable state and are not rewritten by the upgrade.\n- **`cotal deliver` is still a standalone command.** Running the delivery daemon as its own process\n remains supported; it is not restricted to being a child of `cotal up`.\n- **`cotal join` keeps its flags.** In particular `--lifecycle-uid` is not new in 0.49.0. It has\n been required alongside `--creds` since well before this release, and the pairing rule did not\n change here. A scripted external join that worked under 0.48.2 works unchanged.\n\n### What does not migrate\n\n**A credential minted before 0.49.0 cannot be renewed.** Managed agent credentials carry a\n24-hour lifetime and the manager re-signs one once it passes **75%** of its life, ticking every\nquarter of the TTL so a tick always lands inside that window. When the manager reaches a credential\nthat carries no issuance, it refuses to renew it and logs the agent by name:\n\n```\n! managed cred renewal <agent>: renewManagedStaticCred: <agent> carries no issuance;\n a static credential minted before SPEC 13.15 is not renewed under an unbound generation\n - respawn the agent\n - the agent dies loud at this cred's expiry unless it is reminted\n```\n\nSo the fleet comes up fine, runs normally, and then each agent stops at its own credential's\nexpiry, within roughly a day of the upgrade, one at a time rather than together. The refusal is\ndeliberate: the renewal would otherwise have to invent a generation nobody issued, which is the\nstate the release exists to remove.\n\n**Respawn the managed agents as the last step of the upgrade.** For this particular upgrade the\nrespawn is not optional: stopping a 0.48.2 manager ends its agent processes whichever CLI you use,\nfor the reason given under the outage window below. The respawn is how they come back, and it is\nalso what mints each credential as an issuance so it renews from then on. One planned pass over the\nfleet is the whole job. Skipping it leaves agents stopped and, for any credential that survived\ninto 0.49.0 unminted, brings the renewal cliff above a day later, one agent at a time.\n\n### Credentials you minted yourself\n\n**A credential you minted with `cotal mint` is a different case, and it very likely needs\nnothing.** The distinction that matters is not the word \"static\", which covers both. It is **what\nminted the credential and who owns its renewal**. A credential the **manager** minted for an agent\nit spawned carries a lifetime and is renewed by the manager, so it is the subject of everything\nabove. A credential **you** minted with `cotal mint` and handed to an external peer is issued with\n**no expiry at all**, and no manager renews it: it is not in the sweep, so there is no renewal to\nfail. It keeps working after the upgrade, and re-minting it would mean coordinating with a third\nparty for no gain.\n\nThe manager says which one it is holding. Where a credential has no expiry to reach, the sweep\nnames it and moves on rather than refusing:\n\n```\n! managed cred renewal <agent>: credential is unbounded - not renewed\n (a pre-TTL credential stays as minted until respawn)\n```\n\nRe-mint an external peer's credential only if you want it to carry a lifetime, and at a time you\nchoose.\n\n### How to read the boot log\n\nA 0.49.0 manager starting over an existing space may print lines like:\n\n```\n verified evicted: <holder-key> (3/12)\n already verified (durable): <holder-key>\n\u2713 boot self-heal: manager/<id> registration gate reopened at generation <n>\n```\n\nThese are **not** a credential migration, and reading them as one is the most likely way to\nconclude the fleet is fine when it is not. They come from the manager repairing **one** endpoint\nregistration gate that a previous restart left frozen, and they enumerate that single gate's\ncredential-family holders as it verifies each one evicted. `already verified (durable)` on a later\nstart is the repair cursor resuming, not a credential that became durable. The repair is real and\nuseful (it is what previously needed `cotal reconcile-gate` by hand), but it says nothing about\nwhether your agent credentials carry issuances. The renewal refusal above is the signal that does.\n\n### Which side to upgrade first in a split topology\n\nMove the manager first.\n\nThe stores 0.49.0 introduces are created by the **manager** at its own boot, not by the broker.\nThey are create-or-verify and idempotent, so a 0.49.0 manager brings the space's authority stores\nup to the new shape itself, and it does so against whichever broker is answering.\n\nBeing honest about the evidence behind each direction, because they are not equally established:\n\n- **Broker-first was measured on a live 30-agent deployment** (issue #1578). Upgrading the broker\n first locks the old manager out immediately: `cotal up` re-renders the broker's generated config\n from the trust record, and after the restart the still-0.48.2 manager is refused on every\n connection with an `authentication error` naming the Nkey, continuously. That text comes from the\n broker process, not from a Cotal command, so match on its shape rather than on an exact string.\n `cotal ps` reports zero agents while\n the agent processes are still alive, because the manager has lost its view of them, not because\n they died. Upgrading the manager clears it immediately.\n- **Manager-first is reasoned from where the new stores are provisioned**, not from a measured\n fleet upgrade. It is the recommended order because the manager is the component that creates what\n 0.49.0 adds, but it has not been run end to end on a production split topology at the time of\n writing. Treat it as the better-supported order rather than a guaranteed one, and keep the\n rollback below ready either way.\n\nWhichever order you pick, **this is not a rolling upgrade**. Between the two steps the mesh is down\nand the manager cannot see its agents. Go straight through rather than pausing between them, and\nschedule it as an outage window.\n\n### What the window looks like\n\n- **The managed agent processes do not survive step 1, in either order.** This is the one place\n where the obvious reordering does not rescue you, so it is worth understanding rather than\n working around. Sparing agents on a bare manager stop is a **handshake**: a 0.49.0 manager\n publishes a capability file proving it can release its agents, and a 0.49.0 `cotal down` refuses\n the stop unless it finds one. **A 0.48.2 manager never publishes that file**, because the\n mechanism ships in the release you are installing. So the old CLI against the old manager sends a\n plain stop and takes every seat with it, and the new CLI against the old manager either refuses\n (leaving `--with-agents`, which reaps deliberately) or falls to the legacy path, warns that it\n cannot verify the manager can spare its agents, and signals it anyway.\n- **You can confirm which side you are on in one command, without stopping anything.** The flag that\n marks the newer behaviour is absent from the older CLI, and its summary line makes the difference\n plain:\n\n ```\n $ cotal down --help # on 0.48.2\n cotal down - stop the whole local stack, or name only the components to stop\n\n $ cotal down --help # on 0.49.0\n cotal down - stop the whole local stack (managed agents stay running unless --with-agents), ...\n ```\n\n If your `cotal down --help` does not mention `--with-agents`, stopping the manager stops the\n agents with it.\n- **Therefore the respawn in step 5 is mandatory recovery for this upgrade, not an optional pass.**\n It is also the step that re-mints credentials as issuances, so it is the same action either way.\n Plan the window to include it rather than treating it as cleanup.\n- The **manager's view** of them is lost while the two sides disagree, so `cotal ps` reports zero\n and control commands do not reach seats.\n- **Messages are not delivered** while the mesh is down.\n- The window is as long as it takes to restart the second component, plus the manager's own start.\n It is minutes, not hours, provided you do not stop between the steps.\n- **Nothing self-heals if you stop halfway.** The refusal is continuous until both sides match.\n\n### Snapshot this before you start\n\nTake these while the deployment is still on 0.48.2. The two `cotal` reads are live reads and must\nhappen before anything stops.\n\n- **A filesystem or volume snapshot of both containers**, if your platform offers one. This is the\n only rollback that covers every case, and it is what the reporting deployment used.\n- **`cotal backup create <dir>`**, for the durable space state, **but read the next paragraph before\n you rely on it**: on a split broker and manager topology it is very likely unavailable to you, and\n the volume snapshot above is your actual rollback.\n- **The trust records and credential directory** under `.cotal/auth` on the manager host, including\n the per-space material directory. These are what a re-mint would otherwise have to replace.\n- **A copy of the channel registry**, so you can verify it came back rather than assuming it did:\n `cotal channels list` before and after.\n- **The output of `cotal ps`**, so you know how many seats you expect to see afterwards and can tell\n a lost view from a lost agent.\n\n#### `cotal backup` on a split topology\n\n**`cotal backup create` cannot read a running stack.** It requires a completed cut, and only\n`cotal down --preserve-state` publishes one:\n\n```\n$ cotal backup create ./backup.0482\n\u2717 backup requires a completed cut; run `cotal down --preserve-state` first\n```\n\n**And `cotal down --preserve-state` requires a manager alive on the host you run it from.** It uses\nthat manager to attest that every retained child stopped, and the check is deliberately fail-closed:\na manager that is dead or merely uncertain refuses rather than preserving an unproven cut. The check\nreads a local pidfile, so a **remote** manager does not satisfy it. On a split topology the broker\nhost has no local manager, which means the documented durable-backup path is not available there.\n\n**Measured rather than assumed, at 0.48.2**: the backup refusal above is executed output. The\npreservation requirement is read from `down.ts` at the same tag, where the preserve path asks a\nmanager to prepare an inventory and then requires that manager to be locally alive before it\ncommits. The part not executed end to end is a genuine two-host split, which needs two real hosts.\n\n**What to do instead.** Use the filesystem or volume snapshot of both containers. That is the\nrollback the reporting deployment actually used, it covers the broker's durable state and the\nmanager's credential material together, and it does not depend on either component being able to\nattest for the other. If you want `cotal backup` as well, take it from a host that does have a live\nlocal manager, and understand it is a second copy rather than the primary rollback.\n\n**This looks like a product limitation rather than a documentation gap**, and it is written here as\none so an operator is not left thinking they mis-typed a command. The upgrade path for the exact\ntopology this page is addressed to cannot use the documented backup command.\n\n### The upgrade end to end\n\n```bash\n# 0. on 0.48.2, STILL RUNNING: record what you expect to see afterwards.\n# These two are live reads, so they must happen before anything stops.\ncotal channels list > channels.before\ncotal ps > ps.before\n\n# 1. manager host. READ THE NOTE BELOW THE BLOCK FIRST: this step ends the\n# managed agent processes whichever order you choose, and the respawn in\n# step 5 is how they come back. It is recovery, not tidying.\n#\n# STOP THE MANAGER WITH THE 0.48.2 CLI, BEFORE INSTALLING 0.49.0. The\n# order matters and it is not recoverable once you install: a 0.49.0\n# `down manager` REFUSES to stop a 0.48.2 manager whose pid record carries\n# a start token, which is every manager on a platform that can read one\n# (Linux can):\n# refusing bare manager stop: ... does not prove this manager can detach\n# its agents; use --with-agents or stop the agents explicitly\n# The refusal names two remedies and NEITHER clears it for this case. The\n# check reads a capability file that only a 0.49.0 manager writes; it never\n# counts agents, so stopping them first changes nothing. And `--with-agents`\n# is whole-stack only, so `down manager --with-agents` is refused by its own\n# flag rule. See #1592.\ncotal down manager # the 0.48.2 CLI, still installed.\n # 0.48.2 has no --with-agents; this\n # is the whole route. On a host that\n # runs the whole stack, the 0.49.0\n # `cotal down --with-agents` after\n # installing is the alternative.\nnpm install -g cotal-ai@0.49.0 # ONLY after the stop above\n# `supervise` RUNS IN THE FOREGROUND and holds the terminal until you stop\n# it. There is no --detach on this command. Start it under whatever keeps\n# your manager alive normally (systemd unit, container entrypoint, or a\n# second terminal), and run the remaining steps from another shell.\ncotal supervise --space <space> --server nats://<broker>:4222\n\n# 2. broker host: stop the stack.\n# NOT `--preserve-state` on a split topology: it needs a manager alive on\n# THIS host to attest its children stopped, and yours is on the other one.\n# Your rollback is the volume snapshot from \"Snapshot this before you\n# start\", not `cotal backup`.\n# See \"cotal backup on a split topology\" above.\ncotal down\n\n# 3. broker host: install 0.49.0 and start it again\nnpm install -g cotal-ai@0.49.0\n# Record the manager log's size BEFORE starting, so step 3a can tell THIS\n# boot's output from every earlier one. It must be captured here, ahead of\n# the start: taken afterwards it sits past the new line and the wait hangs.\n# `<spaceKey>` is NOT the space name. It is lowercase hex of the name's\n# UTF-8 bytes, so space `prod` is `manager.70726f64.log`. Do not guess it:\n# `cotal up` prints the real path on its launch line. Substituting the\n# plain name points at a file that does not exist, and the wait below then\n# burns its full timeout before telling you.\nLOG=.cotal/manager.<spaceKey>.log\nOFF=$( [ -f \"$LOG\" ] && wc -c < \"$LOG\" || echo 0 )\ncotal up --detach --host 0.0.0.0 --space <space> --no-manager\n\n# 3a. SPLIT TOPOLOGY ONLY: `--no-manager` above boots the broker (and the\n# delivery daemon) with NO local manager on the broker host, so there is\n# no wait-and-stop step on a current cotal-ai. The rest of this step is\n# the OLDER-host recipe, kept because the flag is refused there and that\n# refusal is your signal you are on it: without the flag the `up` also\n# starts a local manager, and you must wait for the log to show it is up,\n# then stop it, or you finish the upgrade with two managers and the one\n# you did not intend is the one nobody is watching.\n# A bare `grep -q` does NOT wait: it reads once and exits 1 immediately\n# if the line has not been written yet. Bound the wait instead, so a\n# manager that never comes up fails loudly rather than reading as ready.\n# The log is opened APPEND-ONLY, so on any host that has run a manager\n# before, this file ALREADY carries a `manager up` line from an earlier\n# boot. Grepping the whole file therefore matches instantly and waits for\n# nothing. Read only what THIS boot appended, using the $OFF captured in\n# step 3 above (before the start, which is the only point it is correct):\ntimeout 60 bash -c \\\n \"until tail -c +$((OFF+1)) \\\"$LOG\\\" | grep -q '. manager up'; do sleep 1; done\"\n# exit 0 = THIS boot logged it; exit 124 = it never did, so STOP and look.\n# This manager is 0.49.0 and publishes its own spare-capability file, so\n# the bare stop below is NOT the refusal case from step 1.\ncotal down manager # broker + delivery remain\n# On a current cotal-ai the two commands above are unnecessary (nothing\n# to wait for, nothing to stop) and `cotal down manager` simply reports\n# no manager to stop.\n\n# 4. verify the mesh is whole again before touching the fleet.\n# Do NOT compare `cotal ps` against ps.before yet: step 1 ended the agent\n# processes, so at this point it is EXPECTED to be empty, and an empty\n# `ps` is also the signature of the broker/manager mismatch described\n# above. The two are indistinguishable here, so compare what the mesh\n# itself should have carried across instead:\ncotal channels list # compare against channels.before: this SHOULD match now\ncotal ps # expect it to be EMPTY here; ps.before is the target for\n # step 5, not for this step\n\n# 5. the step that is easy to skip: respawn the managed agents so their\n# credentials are re-minted as issuances and can renew. Persona is a\n# POSITIONAL argument here, unlike `cotal stop`, which requires --name.\n# One call per agent:\ncotal spawn <persona> --detach --name <n> --space <space>\n# then the comparison step 4 could not make:\ncotal ps # NOW compare against ps.before: seat count should match\n```\n\nThe mesh is down from step 2 until step 3 finishes. That is the window. On a split topology there is\nno cut and no backup inside it, so the window is the stop, the install and the restart, nothing more.\n\n## Adding a section for a future release\n\n**Every changeset marked breaking adds a section to this page.** A release that changes what an\noperator must do, in what order, or what stops working, is not finished until the section exists.\n`scripts/upgrade-section-gate.mjs` grades a commit range for this: run it as\n`pnpm upgrade-section-gate --base <ref>` and it reds when the range carries a breaking change and\nadds no new release section. The former Attribution workflow enforced this on each pull request;\nthat workflow and its required-status rule were removed on 22 October 2026. The script remains\navailable for manual checks, and reviewers still check upgrade instructions for breaking changes.\nBe precise about what the check proves. It proves a section for a release\n**was written here**. It cannot prove the section is **correct**, or that it describes the break\nthat actually landed, and it cannot see a breaking change that carries no marker at all. Reviewing\nthe words remains a person's job.\n\n**Mark the break, or the gate cannot see it.** Any one of these is enough, and they are the only\nthings it reads:\n\n- a `!` before the colon in the commit subject, as in `feat(core)!: bind hosted runs to the caller`\n- a `BREAKING CHANGE:` footer in the commit body\n- a changeset in `.changeset/` declaring a `major` bump for any package\n\nThe marker must survive the squash. A `!` that lives only in a commit you squash away is not in the\nrange the gate grades, so put it in the subject that lands on `main`.\n\n**The heading is a `##` and names the release**, like `## From 0.48.2 to 0.49.0`. Both matter, and\nneither is a style preference. Coverage is claimed by a heading, so a heading that names\nno release claims every release and distinguishes none: `## Notes` with a sentence under it would\notherwise satisfy the rule. Naming the release also makes the section the one an operator upgrading\nthat release will search for. Use `###` freely for detail inside a section. Subsections belong to\ntheir release rather than counting as separate coverage.\n\nName the release that first carries the change: the next version Changesets publishes, which\n`pnpm changeset status --verbose` lists. `bin/package.json` on `main` still reads the release already\npublished. If a release is cut while the change is open, the change ships in the release after it,\nso move the heading before merging. The gate accepts any version in a heading, so before merging a\nrelease pull request, check every heading added since the previous tag against the version it\npublishes.\n\nA section is written for the operator, not for the reviewer. It answers, in this order:\n\n1. What keeps working with no action at all.\n2. What does **not** migrate, and when that becomes visible. Name the log line if there is one.\n3. The order to move components in for a split topology, and why that order.\n4. What the outage window looks like, including what survives it.\n5. What to snapshot before starting.\n6. The commands, end to end.\n\n**Where an answer was not measured, say so in the document rather than guessing.** An operator who\nknows which half of a recommendation is reasoned and which is measured can plan around it; one who\nfinds out afterwards cannot.\n\n## Handle redemption in 0.91.0\n\n`auth redeem <private-handle-file>` selects the native auth target, then refuses unsupported\nconferring-handle redemption without reading the file or issuing credentials. It does not redirect\nthe artifact into agent enrollment or interactive-session redemption. Offline handle inspection\nstill labels signatures, revocation and holder currency not checked. An explicit `--space`\nrequests static-registry signature inspection with a native ephemeral operator reader. It checks\nsignatures, issuer scope, presented links and required revocation records without establishing\npresenter identity, live epochs or use authority. It confers no holder credential, persists no\nreader credential, and never turns missing or stale native records into verified offline success.\nOpen/user-auth inspection and conferring redemption remain unsupported.\n\n## Dashboard service transition in 0.91.0\n\nThe matching dashboard package now registers a service preset and hidden native child, not a\npublic web command. An older cached command descriptor is hidden and refuses with the matching\nextension-update remedy before importing provider code or repairing the install. `update` remains\nexplicit; `service start web` never installs a deliberately absent extension.\n\nConfigure the saved web definition with its existing registered static-auth root, one protected\ncredential file and explicit read-only intent. The service retains required TLS, creates no caller\nor channel-purger credential, opens no browser and prints no bootstrap secret into logs. The page\nmarks it read-only and hides delete; the server independently rejects that mutation before any\nalternate connection. The private session file holds the full-launch bootstrap; logs give its\nreference only. User-auth/open targets and OS installation remain unsupported in this route.\nExisting legacy process records are still observed, never silently adopted. An already-running\nlegacy dashboard is not restarted or converted by loading metadata. See [dashboard setup](watch-a-mesh.md#web-dashboard).\n\n## Registered lifecycle retirement in 0.91.0\n\n`auth retire <full-lifecycle-uid> [--yes]` has closed UID/space input, but the standalone CLI still\nlacks a supported native retirement instrument carrying its own current serve identity and gate.\nIt refuses without publishing a request. It does not borrow the manager's identity, treat --yes as\na grant, or call the host's interactive/managed loopback doors as a substitute. This remains an\nexplicit authority gate, not a completed retirement operation.\n\n## Local-process `rootedAt` removal in 0.91.0\n\nA local-process extension descriptor no longer has a `rootedAt` field. No stop has read it since\nthe dashboard became a service. Operators have nothing to migrate, snapshot or order, and there is\nno outage: a manifest that an older install cached with the field still loads, and the next\nmanifest rewrite drops it. An extension author removes the field from each `LocalProcess`\ndeclaration, where TypeScript now reports it as an unknown property.\n"
|
|
65480
65480
|
},
|
|
65481
65481
|
{
|
|
65482
65482
|
"slug": "watch-a-mesh",
|